npm 脚本执行

npm 脚本执行

先看一段代码:

1
2
3
4
5
// egg package.json
"scripts": {
"start": "egg-scripts start --daemon --title=egg-server-example",
"stop": "egg-scripts stop --title=egg-server-example"
}

然后问几个问题:

  • egg-scripts 没有全局安装,怎么能使用?
  • 怎么没有类似 bin/www.js 的启动文件?
  • –daemon –title=egg-server-example 怎么起效果?

如果你对这些很模糊,就有必要看下这里解释了。

npm bin

node_modules 执行环境

Q1: egg-scripts 没有全局安装,怎么能使用?

既然没有执行 npm install egg-scripts -g ,为什么能找到这个插件,不是应该报这种错误么?

1
-bash: xx: command not found

首先要知道 npm install xx -g 怎么能使得某命令全局范围生效?

先查看下 系统环境变量

1
2
shixinghaodeMacBook-Pro:bin shixinghao$ env
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

然后再看对应的 /usr/local/bin/ 下有哪些程序链接:

1
2
3
4
shixinghaodeMacBook-Pro:bin shixinghao$ cd /usr/local/bin/
shixinghaodeMacBook-Pro:bin shixinghao$ ls
cnpm ng npm yarn
egg-init node npx yarnpkg

这就是控制台键入 node -v 能输出版本号等信息的原因。

然后再看下我们安装的 global modules ,用 ls -l 查看下会发现全局包最终的原地址就是 /usr/local/lib/node_modules/ 下  的文件(其实就是超链接)。因为有了这些定义好的系统环境上的变量,就能在控制台输入 egg-init 等命令就能执行相关程序文件。

要完全回答第一个问题,还要继续看下个点。

package.json bin 配置

那么我运行本地安装的 node_modules 可不可行呢?其实这很打脸,明显不行。但不妨一试。

1
2
3
// /egg-example/node_modules/ademo-bin 自定义的包
shixinghaodeMacBook-Pro:egg-example shixinghao$ ademo-bin
bash: ademo-bin: command not found

如果想要本地安装的 modules 和全局安装和相同的效果,就要通过 npm 来实现和系统环境一样的逻辑,接下来就要了解下 npm bin 相关机制。

Print the folder where npm will install executables.

有了上面解释 环境变量 一些概念加上 bin 的一些解释,很容易明白 .bin 文件夹的含义。

简单说安装好本地 modules 后,npm 会自动把 modules bin 文件扔到 node_modules/.bin 下。我们可以检查下已有的 npm bin 配置:

1
2
3
4
shixinghaodeMacBook-Pro:egg-example shixinghao$ npm bin
/Applications/eminoda/github_project/egg-example/node_modules/.bin
shixinghaodeMacBook-Pro:egg-example shixinghao$ npm bin -g
/usr/local/bin

来翻下 egg 项目 .bin 目录:

发现了 egg-script 的踪迹,那么问题又来了,怎么就自动添加到 .bin 中呢?

原理不说了,可以直接看这个链接 https://docs.npmjs.com/files/package.json#bin ,下面实际操作下:

这里写了个简单的 module:demo-bin-test,用来测试 bin 配置。

npm -i demo-bin-test 后,可以看到在 package.json 维护的 bin 属性。

1
2
3
"bin": {
"demo-bin-test": "bin/index.js"
}

这个配置很重要,在 npm install 的时候就解析这句配置,然后在 node_modules/.bin 添加我们定义的 demo-bin-test 命令脚本。在执行该命令时,就是先从 npm 的本地 bin 环境作用域开始寻找,再从 global 范围寻找。

1
2
3
"scripts": {
"start": "demo-bin-test"
}

然后你可以直接运行 npm start 看会出现什么效果?

调用了 /node_modules/demo-bin-test/bin/index.js 本地文件。到此就解释了第一个问题。

egg 的启动文件

怎么没有类似 bin/www.js 的启动文件?

通常我们都是定义如下的程序启动文件:

1
2
3
"scripts":{
"start":"node ./bin/www.js" // npm [run] start 运行项目下 /bin/www.js 文件,从而开启应用
}

但在举例的 script start 却没有启动文件,这就牵扯到 egg 运行机制上的一些东西,简单说 入口文件 被 egg 所封装起来。

虽然本文不是详细介绍 egg ,但理解透彻这完整的 script 脚本很有必要,简单说明下怎么找到真实的入口 js:

  1. 运行启动文件

    当然没有全局安装过 egg-scripts ,后面解释

    1
    "start": "egg-scripts start ..."
  2. 运行 start-cluster

    进入到本地 node_modules 找到 egg-scripts ,其实核心通过继承 Command 实现了自己脚本命令的方式(第三个问题在做说明)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // /egg-example/node_modules/egg-scripts/lib/cmd/start.js
    class StartCommand extends Command {
    constructor(rawArgv) {
    // 注意这个文件
    this.serverBin = path.join(__dirname, '../start-cluster');
    }
    // 初始化的时候运行
    * run(context) {
    ...
    const eggArgs = [ ...(execArgv || []), this.serverBin, clusterOptions, `--title=${argv.title}` ];
    ...
    // child_spawn 开启新的子命令窗口,执行相关 args
    const child = this.child = spawn(command, eggArgs, options);
    ...
    }
    }

    运行 start-cluster 文件

    1
    2
    // /egg-example/node_modules/egg-scripts/lib/start-cluster
    require(options.framework).startCluster(options);

    startCluster 这个方法其实就是在 egg 中定义的

    1
    2
    // /egg-example/node_modules/egg/index.js
    exports.startCluster = require('egg-cluster').startCluster;
  3. egg

    找着找着,你就会发现其实就是调用 app_worker

    1
    2
    3
    // /Applications/eminoda/github_project/egg-example/node_modules/egg-cluster/lib/app_worker.js
    const Application = require(options.framework).Application;
    ...

    备注下:framework 其实就是前面在 egg-script 中定义好的 egg modules 的框架路径

    到这里基本也能猜到后续怎么回事了,更深层次的内容请各位看官线下继续学习 :grimacing:

Command

再来看最后个问题:

–daemon –title=egg-server-example 怎么起效果?

这用到了 command-bin 模块

Build a bin tool for your team

You maybe need a custom xxx-bin to implement more custom features.

具体怎么回事,同样可以参考如下的 demo 示例(demo-bin-test):

继承 Command ,定义相关配置(控制台使用说明文件、加载命令脚本、别名定义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// /demo-bin-test/bin/command.js
const path = require('path');
const Command = require('common-bin');
class MainCommand extends Command {
constructor(rawArgv) {
super(rawArgv);
this.usage = 'Usage: demo <command> [options]';

// load entire command directory
this.load(path.join(__dirname, 'cmd'));

this.yargs.alias('v', 'version');
}
}

module.exports = MainCommand;

然后执行该 js,就能看到控制台出现如下信息:

1
2
3
4
5
6
7
8
9
10
shixinghaodeMacBook-Pro:demo-bin-test shixinghao$ node ./bin/command.js
Usage: demo <command> [options]

命令:
command.js completion generate bash completion script
command.js start

Global Options:
-h, --help 显示帮助信息 [布尔]
-v, --version, --version 显示版本号 [布尔]

再定义具体命令参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// /demo-bin-test/cmd/start.js
const Command = require('common-bin');
const { spawn } = require('child_process');
class StartCommand extends Command {
constructor(rawArgv) {
super(rawArgv);
this.options = {
name: {
type: 'string',
description: 'project name'
}
};
}
*run({ argv }) {
// input:start --name=test
// console.log(argv)
let ls = spawn('node', ['./say.js'], {});
ls.stdout.on('data', data => {
console.log(`stdout: ${data}`);
});
}
}

module.exports = StartCommand;
1
2
// /demo-bin-test/say.js
console.log('say');

定义一个 start.js ,然后在上面的命令后追加 start 并执行,Command 就会调用 run 方法,输出需要的逻辑:

1
2
shixinghaodeMacBook-Pro:demo-bin-test shixinghao$ node ./bin/command.js start
stdout: say

点到为止,其实就是参照 egg-scripts 写了个简单的 Demo。

最后

因为做其他事情需要,有了开头三个小问题,没想到会牵扯那么多知识点。拖的时间有些长,不过还是蛮有意义的。

参考:

【长按关注】看看↓↓↓?
Eminoda wechat
【前端雨爸】分享前端技术实践,持续输出前端技术文章
欢迎留言,评论交流,一起讨论前端问题
📢 因为是开源博客,为避免 Gitalk授权 带来的 安全风险,也可访问