# egg-bin源码解析笔记
egg-bin
是一个本地开发者工具,集成到egg中,里面涵盖了很多功能,比如调试,单元测试和代码覆盖率等这些功能,可以说是比较强大了。
下面就egg-bin
源码分析一些东西(针对的是4.13.2)
egg-bin如何工作的:
在本地运行egg
项目的时候,我们往往会根据不同的场景(调试、测试等)来选择不同的命令(egg-bin dev、egg-bin debug
)启动项目,从而达到我们需要的效果,但是egg-bin
是如何让命令运行起来的呢?
比如在命令行中回车下面的命令:
$ egg-bin dev --port 7001
开始进入node_modules/egg-bin/bin/egg-bin.js
文件,文件代码比较简单:
#!/usr/bin/env node
'use strict';
const Command = require('..');
new Command().start();
2
3
4
其中,Command
对应的是node_modules/egg-bin/bin.egg-bin.js
中的EEggBin
对象。首先理清以下egg-bin
中对应的几个对象之间的关系,如下图:
其中最后导出的EggBin
对象以及DevCommand、AutodCommand、TestCommand、PkgFilesCommand
继承于egg-bin/lib/command.js
里面导出的Command对象,而egg-bin/lib/command.js
里面导出的Command
又是继承于第三库command-bin (opens new window),而command-bin
中导出的CommandBin对象又是一个yards属性,该属性是目前比较流行的命令工具yargs (opens new window)。DebugCommand
和CovCommand
则分别继承自DevCommand
和TestCommand
。
进入index.js
文件源代码,该文件至少定义了EggBin
这个对象,并且将一些sub command
挂载到EggBin
这个导出对象中,有如下几个自命令:
// load directory
this.load(path.join(__dirname, 'lib/cmd'));
2
- Command --- 继承自 common-bin的基础命令对象
- CovCommand --- 代码覆盖率命令对象
- DevCommand --- 本地开发命令对象
- TestCommand --- 测试命令对象
- DebugCommand --- 调试命令对象
- PkgfilesCommand --- 包文件对象
接着就是执行bin/egg-bin.js
文件中的new Command().start()
这一行,首先会先去执行EggBin
构造函数中的内容:
class EggBin extends Command {
constructor(rawArgv) {
// 获取用户输入的options
super(rawArgv);
this.usage = 'Usage: egg-bin [command] [options]';
// load 对应目录下的command文件
this.load(path.join(__dirname, 'lib/cmd'));
}
}
2
3
4
5
6
7
8
9
10
# 获取命令参数
由于上面的继承关系,第一行就会直接执行到Command-bin/lib/command.js
中的第一行
/**
* original argument
* @type {Array}
*/
this.rawArgv = rawArgv || process.argv.slice(2);
2
3
4
5
此时 this.rawArgv的值如下:
0: "dev"
1: "--port"
2: "7001"
2
3
# load 配置文件
获取到这个参数之后就会直接将该参数传给yargs
并将yyargs
对象赋给自己的一个yargs
属性
/**
* yargs
* @type {Object}
*/
this.yargs = yargs(this.rawArgv);
2
3
4
5
然后就开始load
命令行文件了,通过追踪,也可以发现最后执行的也是common-bin
中的load
命令common-bin
中的load
成员函数,该函数要求参数是所需要获取的命令文件的绝对路径,其中common-bin/command.js
中的load
源码如下:
load(fullPath) {
// 省略对参数的校验
// load entire directory
const files = fs.readdirSync(fullPath);
const names = [];
for (const file of files) {
if (path.extname(file) === '.js') {
const name = path.basename(file).replace(/\.js$/, '');
names.push(name);
this.add(name, path.join(fullPath, file));
}
}
// 省略
}
2
3
4
5
6
7
8
9
10
11
12
13
14
其中files文件的值为egg-bin/lib/cmd下文件名称:
0: "autod.js"
1: "cov.js"
2: "debug.js"
3: "dev.js"
4: "pkgfiles.js"
5: "test.js"
2
3
4
5
6
然后将files进行遍历,执行下面的add的操作:
/**
* add sub command
* @param {String} name - a command name
* @param {String|Class} target - special file path (must contains ext) or Command Class
* @example `add('test', path.join(__dirname, 'test_command.js'))`
*/
add(name, target) {
assert(name, `${name} is required`);
if (!(target.prototype instanceof CommonBin)) {
assert(fs.existsSync(target) && fs.statSync(target).isFile(), `${target} is not a file.`);
debug('[%s] add command `%s` from `%s`', this.constructor.name, name, target);
target = require(target);
assert(target.prototype instanceof CommonBin,
'command class should be sub class of common-bin');
}
this[COMMANDS].set(name, target);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
其中要求 参数target
也是对应的文件的绝对路径。在进行条件判断之后直接使用set
将该命令挂载在this[COMMANDS]
变量中。遍历完成后this[COMMANDS]
的值如下所示:
# 执行start()
最重要的start
操作,追根溯源也是执行的common-bin
里面的start()
, start()
里面主要使用co
包了一个generator
函数,并且在genertor
函数中执行了this[DISPATCH]
,然后,重头戏来了,this[DISPATCH]
的源码如下:
/**
* dispatch command, either `subCommand.exec` or `this.run`
* @param {Object} context - context object
* @param {String} context.cwd - process.cwd()
* @param {Object} context.argv - argv parse result by yargs, `{ _: [ 'start' ], '$0': '/usr/local/bin/common-bin', baseDir: 'simple'}`
* @param {Array} context.rawArgv - the raw argv, `[ "--baseDir=simple" ]`
* @private
*/
* [DISPATCH]() {
// define --help and --version by default
this.yargs
// .reset()
.completion()
.help()
.version()
.wrap(120)
.alias('h', 'help')
.alias('v', 'version')
.group([ 'help', 'version' ], 'Global Options:');
// get parsed argument without handling helper and version
const parsed = yield this[PARSE](this.rawArgv);
const commandName = parsed._[0];
if (parsed.version && this.version) {
console.log(this.version);
return;
}
// if sub command exist
if (this[COMMANDS].has(commandName)) {
const Command = this[COMMANDS].get(commandName);
const rawArgv = this.rawArgv.slice();
rawArgv.splice(rawArgv.indexOf(commandName), 1);
debug('[%s] dispatch to subcommand `%s` -> `%s` with %j', this.constructor.name, commandName, Command.name, rawArgv);
const command = this.getSubCommandInstance(Command, rawArgv);
yield command[DISPATCH]();
return;
}
// register command for printing
for (const [ name, Command ] of this[COMMANDS].entries()) {
this.yargs.command(name, Command.prototype.description || '');
}
debug('[%s] exec run command', this.constructor.name);
const context = this.context;
// print completion for bash
if (context.argv.AUTO_COMPLETIONS) {
// slice to remove `--AUTO_COMPLETIONS=` which we append
this.yargs.getCompletion(this.rawArgv.slice(1), completions => {
// console.log('%s', completions)
completions.forEach(x => console.log(x));
});
} else {
// handle by self
yield this.helper.callFn(this.run, [ context ], this);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
首先会去执行yargs
中的一些方法,这里common-bin
只是保留了yargs
中一些对自己有用的方法,比如completion()、wrap()、alias()
等,具体关于yargs
的API
可以移步这里 (opens new window)。接着是执行this[PARSE]
将rawArgv
进行处理,处理后的parse
对象结构如下:
接着就是对获取到的命令行进行校验,如果存在this[COMMAND]
对象中就执行。在当前例子中也就是去执行DevCommand
。而由于DevCommand
最终也是继承于common-bin
的,然后执行yield command[DISPATCH]()
;又是递归开始执行this[DISPATCH]
了,直到所有的子命令递归完毕,才会去使用helper(common-bin
中支持的异步关键所在)类继续执行每个command
文件中的*run()
函数。
# egg-bin中的子命令文件
dev.js
作为在egg
项目中本地开发最为重要的开发命令,dev.js
无疑肩负着比较重要的指责。在dev.js
中,主要是定义了一些默认的端口号,以及入口命令等。*run
的源码如下:
* run(context) {
const devArgs = yield this.formatArgs(context);
const env = {
NODE_ENV: 'development',
EGG_MASTER_CLOSE_TIMEOUT: 1000,
};
const options = {
execArgv: context.execArgv,
env: Object.assign(env, context.env),
};
debug('%s %j %j, %j', this.serverBin, devArgs, options.execArgv, options.env.NODE_ENV);
yield this.helper.forkNode(this.serverBin, devArgs, options);
}
2
3
4
5
6
7
8
9
10
11
12
13
主要是对当前的上下文参数进行转化并对端口进行了一些处理,然后就开始调用helper
的forkNode
来执行入口命令,其中this.serverBin
的值为:Users/uc/Project/egg-example/node_modules/egg-bin/lib/start-cluster
,下面的事情可以异步这里进行了解:
debug.js
有上分析可知,DebugCommand
继承于DevCommand
,所以在constructor
的时候就会去执行dev
中的一些options
,而且在debug.js
中的*run
函数中直接调用的是dev.js
中的formatArgs()
参数处理。关键源码(有删减)如下:
* run(context) {
const proxyPort = context.argv.proxy;
context.argv.proxy = undefined;
const eggArgs = yield this.formatArgs(context);
//省略部分
// start egg
const child = cp.fork(this.serverBin, eggArgs, options);
// start debug proxy
const proxy = new InspectorProxy({ port: proxyPort });
// proxy to new worker
child.on('message', msg => {
if (msg && msg.action === 'debug' && msg.from === 'app') {
const { debugPort, pid } = msg.data;
debug(`recieve new worker#${pid} debugPort: ${debugPort}`);
proxy.start({ debugPort }).then(() => {
console.log(chalk.yellow(`Debug Proxy online, now you could attach to ${proxyPort} without worry about reload.`));
if (newDebugger) console.log(chalk.yellow(`DevTools → ${proxy.url}`));
});
}
});
child.on('exit', () => proxy.end());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
此处首先是开启egg
,做的是和dev
里面一样的东西,然后则是实例化InspectorProxy
进行debug
操作,在命令行打印出devtools
的地址。
test.js
这个命令主要是用来运行egg
项目中的test
文件的,也就是跑我们自己写的测试用例,关于如何写单元测试,可以异步单元测试 (opens new window),在这个文件,*run
形式也和上面类似,然后调用this.formatTestArgs(),formatTestArgs
源码如下(有删减):
/**
* format test args then change it to array style
* @param {Object} context - { cwd, argv, ...}
* @return {Array} [ '--require=xxx', 'xx.test.js' ]
* @protected
*/
* formatTestArgs({ argv, debugOptions }) {
const testArgv = Object.assign({}, argv);
/* istanbul ignore next */
testArgv.timeout = testArgv.timeout || process.env.TEST_TIMEOUT || 60000;
testArgv.reporter = testArgv.reporter || process.env.TEST_REPORTER;
// force exit
testArgv.exit = true;
// whether is debug mode, if pass --inspect then `debugOptions` is valid
// others like WebStorm 2019 will pass NODE_OPTIONS, and egg-bin itself will be debug, so could detect `process.env.JB_DEBUG_FILE`.
if (debugOptions || process.env.JB_DEBUG_FILE) {
// --no-timeout
testArgv.timeout = false;
}
// collect require
let requireArr = testArgv.require || testArgv.r || [];
/* istanbul ignore next */
if (!Array.isArray(requireArr)) requireArr = [ requireArr ];
// clean mocha stack, inspired by https://github.com/rstacruz/mocha-clean
// [mocha built-in](https://github.com/mochajs/mocha/blob/master/lib/utils.js#L738) don't work with `[npminstall](https://github.com/cnpm/npminstall)`, so we will override it.
if (!testArgv.fullTrace) requireArr.unshift(require.resolve('../mocha-clean'));
requireArr.push(require.resolve('co-mocha'));
if (requireArr.includes('intelli-espower-loader')) {
console.warn('[egg-bin] don\'t need to manually require `intelli-espower-loader` anymore');
} else {
requireArr.push(require.resolve('intelli-espower-loader'));
}
// for power-assert
if (testArgv.typescript) {
// remove ts-node in context getter on top.
requireArr.push(require.resolve('espower-typescript/guess'));
}
testArgv.require = requireArr;
let pattern;
// changed
if (testArgv.changed) {
pattern = yield this._getChangedTestFiles();
if (!pattern.length) {
console.log('No changed test files');
return;
}
}
if (!pattern) {
// specific test files
pattern = testArgv._.slice();
}
if (!pattern.length && process.env.TESTS) {
pattern = process.env.TESTS.split(',');
}
// collect test files
if (!pattern.length) {
pattern = [ `test/**/*.test.${testArgv.typescript ? 'ts' : 'js'}` ];
}
pattern = pattern.concat([ '!test/fixtures', '!test/node_modules' ]);
// expand glob and skip node_modules and fixtures
const files = globby.sync(pattern);
files.sort();
if (files.length === 0) {
console.log(`No test files found with ${pattern}`);
return;
}
// auto add setup file as the first test file
const setupFile = path.join(process.cwd(), 'test/.setup.js');
if (fs.existsSync(setupFile)) {
files.unshift(setupFile);
}
testArgv._ = files;
// remove alias
testArgv.$0 = undefined;
testArgv.r = undefined;
testArgv.t = undefined;
testArgv.g = undefined;
testArgv.typescript = undefined;
return this.helper.unparseArgv(testArgv);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
代码里面的注释很清楚了,就是将单元测试的一些库push
进requireArr
的值如下:
其中mocha-clean
是清除上一次mocha
遗留的堆栈了,后面两个就是egg选用的测试框架和断言库了。
然后就是加载egg项目中除掉node_modules
和fixtures
里面的test
文件,即项目层面的*.test.js
后面也就是开启进程进行单元测试。
cov.js
cov.js
是用来测试代码的覆盖率的。其中CovCommand
继承自TestCommand
,在cov
的*run
中主要定义了字段,比如exclude、nycCli、coverageDir、outputDir
等。根据英文命名就知道是什么意思了。然后继续执行getCovArgs
是对参数的一些处理,源码也就很简单,就不贴出来了,在getCovArgs
中将上面test.js
中的承诺书一起concat
进来了,最后返回的covArgs
的样子是这样的:
然后又是开启进程了。
autod.js和pkgfiles.js
这两个比较简单,这里就不再赘述了
# 总结
整个egg-bin
看下来,还是很厉害的,涉及的都是我之前没听过或者听过但是没用过的高大尚的东西,比如commander.js,yargs,mocha,co-mocha,power-assert,istanbuljs,nyc,