前面,我们学习了common-bin这个包。接下来呢,我们开始了解egg-bin。
那从哪里开始呢?我们使用egg初始化项目,在scripts中,我们发现其启动命令是这样的。
"dev": "EGG_SERVER_ENV=local egg-bin dev",
所有我们从这个脚本开始,看egg-bin dev做了什么。
egg-bin
egg-bin是egg框架的配套开发工具一环,继承于command-bin(更底层、通用的抽象)。
源码解析
package.json
首先我们看下package.json中包的描述信息。
"main": "index.js",
"bin": {
"egg-bin": "bin/egg-bin.js",
"mocha": "bin/mocha.js",
"ets": "bin/ets.js",
"c8": "bin/c8.js"
},
bin/egg-bin.js
这里我们暂时只关注egg-bin,顺藤摸瓜,找到bin/egg-bin.js。
#!/usr/bin/env node
'use strict';
const Command = require('..');
// 先实例化、后start
new Command().start();
index.js
Command来自入口文件index.js,代码如下:
'use strict';
const path = require('path');
const Command = require('./lib/command');
class EggBin extends Command {
constructor(rawArgv) {
super(rawArgv);
this.usage = 'Usage: egg-bin [command] [options]';
// load directory
this.load(path.join(__dirname, 'lib/cmd'));
}
}
module.exports = exports = EggBin;
exports.Command = Command;
exports.CovCommand = require('./lib/cmd/cov');
exports.DevCommand = require('./lib/cmd/dev');
exports.TestCommand = require('./lib/cmd/test');
exports.DebugCommand = require('./lib/cmd/debug');
exports.PkgfilesCommand = require('./lib/cmd/pkgfiles');
逻辑:
- EggBin作为对外暴露的上层类会被初始化。
- 初始化
usage说明。 - 加载
lib/cmd下文件,并且以文件名注册子命令。
- 初始化
- 初始化
Command,来自/lib/command。
我们其实可以看下注册子命令后其实就长这个样子。
你可以看到Usage: egg-bin [command] [options]都是可以对得上。
lib/command.js
'use strict';
const path = require('path');
const fs = require('fs');
const BaseCommand = require('common-bin');
class Command extends BaseCommand {
constructor(rawArgv) {
super(rawArgv);
this.parserOptions = {
execArgv: true,
removeAlias: true,
};
// common-bin setter, don't care about override at sub class
// https://github.com/node-modules/common-bin/blob/master/lib/command.js#L158
this.options = {
typescript: {
description: 'whether enable typescript support, will load tscompiler on startup',
type: 'boolean',
alias: 'ts',
default: undefined,
},
declarations: {
description: 'whether create dts, will load `egg-ts-helper/register`',
type: 'boolean',
alias: 'dts',
default: undefined,
},
tscompiler: {
description: 'ts compiler, like ts-node、ts-eager、esbuild-register etc.',
type: 'string',
alias: 'tsc',
default: undefined,
},
};
}
/**
* default error handler
* @param {Error} err - err obj
*/
errorHandler(err) {
console.error(err);
process.nextTick(() => process.exit(1));
}
get context() {
const context = super.context;
const { argv, debugPort, execArgvObj, cwd, env } = context;
// compatible
if (debugPort) context.debug = debugPort;
// remove unuse args
argv.$0 = undefined;
// read package.json
// 挂载应用程序的根路径,默认为 process.cwd()。
let baseDir = argv.baseDir || cwd;
if (!path.isAbsolute(baseDir)) baseDir = path.join(cwd, baseDir);
const pkgFile = path.join(baseDir, 'package.json');
const pkgInfo = fs.existsSync(pkgFile) ? require(pkgFile) : null;
const eggInfo = (pkgInfo && pkgInfo.egg) || {};
// 默认空数组,从来存储从应用requre的模块
execArgvObj.require = execArgvObj.require || [];
// read `egg.typescript` from package.json if not pass argv
if (argv.typescript === undefined && typeof eggInfo.typescript === 'boolean') {
argv.typescript = eggInfo.typescript;
}
// 如果命令行没有传递,就从package.json 读取 `egg.declarations`
if (argv.declarations === undefined && typeof eggInfo.declarations === 'boolean') {
argv.declarations = eggInfo.declarations;
}
// 如果命令行没有传递,就从package.json 读取 `egg.tscompiler`
// 比如 tscompiler: '/Users/xxx/Documents/github-source/egg-bin/node_modules/ts-node/register/index.js',
// try to load from `cwd` while tscompipler has value or app has ts-node deps
if (argv.tscompiler === undefined && !eggInfo.tscompiler) {
const useAppTsNode = pkgInfo && (
(pkgInfo.dependencies && pkgInfo.dependencies['ts-node']) ||
(pkgInfo.devDependencies && pkgInfo.devDependencies['ts-node'])
);
// 优先从你的应用中读取ts-node
argv.tscompiler = require.resolve('ts-node/register', useAppTsNode ? { paths: [ cwd ] } : undefined);
} else {
argv.tscompiler = argv.tscompiler || eggInfo.tscompiler;
argv.tscompiler = require.resolve(argv.tscompiler, { paths: [ cwd ] });
}
// read `egg.require` from package.json
if (eggInfo.require && Array.isArray(eggInfo.require)) {
execArgvObj.require = execArgvObj.require.concat(eggInfo.require);
}
// load ts-node
if (argv.typescript) {
execArgvObj.require.push(argv.tscompiler);
// tell egg loader to load ts file
env.EGG_TYPESCRIPT = 'true';
// load files from tsconfig on startup
env.TS_NODE_FILES = process.env.TS_NODE_FILES || 'true';
}
// load egg-ts-helper
if (argv.declarations) {
execArgvObj.require.push(require.resolve('egg-ts-helper/register'));
}
return context;
}
}
module.exports = Command;
这里实现逻辑如下:
constructor内按需自定义this.parserOptions,覆盖基类,定义好yargs options。也就是
Options:
--typescript, --ts whether enable typescript support, will load tscompiler on startup [boolean]
--declarations, --dts whether create dts, will load `egg-ts-helper/register` [boolean]
--tscompiler, --tsc ts compiler, like ts-node、ts-eager、esbuild-register etc.
- 实现自己的错误处理
errorHandler。
// 允许所有同步方法执行完之后,下一个事件循环开始之前执行
process.nextTick(() => process.exit(1));
- 实现自己的
context。
- 从基类获取
context
const context = super.context;
const { argv, debugPort, execArgvObj, cwd, env } = context;
// 实际如下:
{
argv: {
_: [],
typescript: undefined,
ts: undefined,
declarations: undefined,
dts: undefined,
tscompiler: undefined,
tsc: undefined,
'$0': 'egg-bin',
help: undefined,
h: undefined,
version: undefined,
v: undefined
},
cwd: '/Users/aedanxu/Documents/github-source/egg-bin',
env: {},
rawArgv: [],
execArgvObj: {}
- 根据
baseDir读取你项目的package.json中egg的配置,处理一些逻辑,具体见上述代码中注释。因此你可以在你的项目packgae.json进行配置,开启-- option。
"egg": {
"declarations": true
},
-
存储从
package.json中读取的egg.require,比如tscompiler(来自你应用的ts-node/require).
lib/cmd/dev.js
现在我们开始看这个egg-bin dev子命名的主要逻辑了。
constructor(rawArgv) {
super(rawArgv);
this.usage = 'Usage: egg-bin dev [dir] [options]';
this.defaultPort = 7001;
this.serverBin = path.join(__dirname, '../start-cluster');
this.options = {
baseDir: {
description: 'directory of application, default to `process.cwd()`',
type: 'string',
},
workers: {
description: 'numbers of app workers, default to 1 at local mode',
type: 'number',
alias: [ 'c', 'cluster' ],
default: 1,
},
port: {
description: 'listening port, default to 7001',
type: 'number',
alias: 'p',
},
framework: {
description: 'specify framework that can be absolute path or npm package',
type: 'string',
},
require: {
description: 'will add to execArgv --require',
type: 'array',
alias: 'r',
},
};
}
get description() {
return 'Start server at local dev mode';
}
构造器的逻辑,我们通过执行egg-bin dev -help就大致看的出来了。
唯一我们需要关注的有
// 默认启动的端口
this.defaultPort = 7001;
// 存储serverBin脚本的执行路径
this.serverBin = path.join(__dirname, '../start-cluster');
#!/usr/bin/env node
'use strict';
const debug = require('debug')('egg-bin:start-cluster');
const options = JSON.parse(process.argv[2]);
debug('start cluster options: %j', options);
require(options.framework).startCluster(options);
start()
各个类初始化流程都走完之后,回到我们的start()。
// bin/egg-bin.js
new Command().start();
start()其实来自基类command-bin的实现,里面大致就是完成yargs初始化、注册子命令等。详细可以看之前文章,最重要的是会重新执行子命令的run方法。
run()
* run(context) {
const devArgs = yield this.formatArgs(context);
// egg相关环境变量
const env = {
NODE_ENV: 'development',
EGG_MASTER_CLOSE_TIMEOUT: 1000,
};
const options = {
// 命令行option相关参数
execArgv: context.execArgv,
env: Object.assign(env, context.env),
};
debug('%s %j %j, %j', this.serverBin, devArgs, options.execArgv, options.env.NODE_ENV);
const task = this.helper.forkNode(this.serverBin, devArgs, options);
this.proc = task.proc;
yield task;
}
* formatArgs(context) {
const { cwd, argv } = context;
/* istanbul ignore next */
argv.baseDir = argv.baseDir || cwd;
/* istanbul ignore next */
if (!path.isAbsolute(argv.baseDir)) argv.baseDir = path.join(cwd, argv.baseDir);
argv.port = argv.port || argv.p;
argv.framework = utils.getFrameworkPath({
framework: argv.framework,
baseDir: argv.baseDir,
});
// remove unused properties
argv.cluster = undefined;
argv.c = undefined;
argv.p = undefined;
argv._ = undefined;
argv.$0 = undefined;
// auto detect available port
if (!argv.port) {
debug('detect available port');
const port = yield detect(this.defaultPort);
if (port !== this.defaultPort) {
argv.port = port;
console.warn(`[egg-bin] server port ${this.defaultPort} is in use, now using port ${port}\n`);
}
debug(`use available port ${port}`);
}
return [ JSON.stringify(argv) ];
}
这里的实现逻辑:
formatArgs():格式化、组装egg startCluster需要的参数args, 并最后其更改为json字符串。
比如
{
_: undefined,
typescript: undefined,
ts: undefined,
declarations: true,
dts: undefined,
tscompiler: '/Users/xx/Documents/github-source/egg-bin/node_modules/ts-node/register/index.js',
tsc: undefined,
workers: 1,
c: undefined,
cluster: undefined,
'$0': undefined,
help: undefined,
h: undefined,
version: undefined,
v: undefined,
p: undefined,
r: undefined,
// 你的egg应用根目录
baseDir: '/Users/xxx/Documents/workplace/xxx-datatalk',
// 应用端口
port: undefined,
// 你的应用egg module
framework: '/Users/xxx/Documents/workplace/xxx-datatalk/node_modules/egg'
}
-
run():最重要逻辑,拿到组装好的参数,执行child_process.fork()(基于promise封装了)。 -
forkNode()
const childs = new Set();
let hadHook = false;
function gracefull(proc) {
// save child ref
childs.add(proc);
// only hook once
/* istanbul ignore else */
if (!hadHook) {
hadHook = true;
let signal;
// 监听一些事件信号, 可参考https://man7.org/linux/man-pages/man7/signal.7.html
[ 'SIGINT', 'SIGQUIT', 'SIGTERM' ].forEach(event => {
process.once(event, () => {
signal = event;
process.exit(0);
});
});
process.once('exit', () => {
// had test at my-helper.test.js, but coffee can't collect coverage info.
for (const child of childs) {
debug('kill child %s with %s', child.pid, signal);
child.kill(signal);
}
});
}
}
exports.forkNode = (modulePath, args = [], options = {}) => {
options.stdio = options.stdio || 'inherit';
debug('Run fork `%s %s %s`', process.execPath, modulePath, args.join(' '));
const proc = cp.fork(modulePath, args, options);
// 对fork出来的进程绑定一些一次性的事件监听
gracefull(proc);
// 基于promise的封装。
const promise = new Promise((resolve, reject) => {
proc.once('exit', code => {
childs.delete(proc);
if (code !== 0) {
const err = new Error(modulePath + ' ' + args + ' exit with code ' + code);
err.code = code;
reject(err);
} else {
resolve();
}
});
});
// 挂载到promise.proc
promise.proc = proc;
return promise;
};
该方法其实指的就是child_process.fork。
所以前面的逻辑你发现大多数都是为了这个api组装所需要的参数。比如modulePath就是前文提到的/Users/xxx/Documents/github-source/egg-bin/lib/start-cluster文件。
所以最终引出了下一章的主题:
require(options.framework).startCluster(options);
egg框架的startCluster()里面做了什么呢?
最后推广下我的公众号: