大家好,我是码农小余。很久不见,怪是想念。
最近在整一个 OpenAPI 编排器,想到 npm-run-all 的任务流。看了一下这个 6 年前的源码。npm-run-all[1] 是一个用来并行或者串行运行多个 npm 脚本的 CLI 工具。阅读完本文,你能收获到:
- 了解整个流程概览;
- 了解核心模块逻辑,入口分析、参数解析、任务流、任务执行等;
流程概览
直入主题,整个 npm-run-all 的整体执行流程如下:
当我们在终端敲入命令,实际上会去调用 bin/xx/index.js 函数,然后调用 bootstrap 去分发不同命令不同参数的逻辑。help 和 version 比较简单,本文不做分析。任务控制方面,会先调用 npmRunAll 做参数解析,然后执行 runTasks 执行任务组中任务,全部任务执行后返回结果,结束整个流程。
入口分析
npm-run-all
包支持三条命令,我们看到源码根目录的 package.json 文件:
{
"name": "npm-run-all",
"version": "4.1.5",
"description": "A CLI tool to run multiple npm-scripts in parallel or sequential.",
"bin": {
"run-p": "bin/run-p/index.js",
"run-s": "bin/run-s/index.js",
"npm-run-all": "bin/npm-run-all/index.js"
},
"main": "lib/index.js",
"files": [
"bin",
"lib",
"docs"
],
"engines": {
"node": ">= 4"
}
}
bin 下面定义的命令脚本:
- run-p,简化使用的脚本,代表并行执行脚本;
- run-s,简化使用的脚本,代表串行执行脚本;
- npm-run-all,复杂命令,通过 --serial 和 --parallel 参数实现前两者一样的效果。
直接看到 bin/npm-run-all/index.js
:
require("../common/bootstrap")("npm-run-all")
上述代码中,如果是执行 run-p 这条命令,则函数传入的参数是 run-p
,run-s
同理。bootstrap 通过参数的不同,将任务分发到 bin 下不同目录中:
.
├── common
│ ├── bootstrap.js
│ ├── parse-cli-args.js
│ └── version.js
├── npm-run-all
│ ├── help.js
│ ├── index.js
│ └── main.js
├── run-p
│ ├── help.js
│ ├── index.js
│ └── main.js
└── run-s
├── help.js
├── index.js
└── main.js
照着上述代码结构,结合 ../common/bootstrap
代码:
"use strict"
module.exports = function bootstrap(name) {
const argv = process.argv.slice(2)
switch (argv[0]) {
case undefined:
case "-h":
case "--help":
return require(`../${name}/help`)(process.stdout)
case "-v":
case "--version":
return require("./version")(process.stdout)
default:
// https://github.com/mysticatea/npm-run-all/issues/105
// Avoid MaxListenersExceededWarnings.
process.stdout.setMaxListeners(0)
process.stderr.setMaxListeners(0)
process.stdin.setMaxListeners(0)
// Main
return require(`../${name}/main`)(
argv,
process.stdout,
process.stderr
).then(
() => {
// I'm not sure why, but maybe the process never exits
// on Git Bash (MINGW64)
process.exit(0)
},
() => {
process.exit(1)
}
)
}
}
bootstrap 函数依据调用的命令调用不同目录下的 help、version 或者调用 main 函数,达到了差异消除的效果。
然后再把目光放在 process.stdout.setMaxListeners(0)
这是啥玩意?打开 issue 链接[2],通过报错信息和翻阅官方文档:
By default
EventEmitter
s will print a warning if more than10
listeners are added for a particular event. This is a useful default that helps finding memory leaks. Theemitter.setMaxListeners()
method allows the limit to be modified for this specificEventEmitter
instance. The value can be set toInfinity
(or0
) to indicate an unlimited number of listeners. 默认情况下,如果为特定事件添加了超过 10 个侦听器,EventEmitters 将发出警告。这是一个有用的默认值,有助于发现内存泄漏。emitter.setMaxListeners() 方法允许为这个特定的 EventEmitter 实例修改限制。该值可以设置为 Infinity(或 0)以指示无限数量的侦听器。
为什么要处理这个情况呢?因为用户可能这么使用:
代码语言:javascript复制$ run-p a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11
你永远想象不到用户会怎么使用你的工具!
参数解析
分析完不同命令的控制逻辑,我们进入核心的 npmRunAll 函数,参数解析部分逻辑如下:
代码语言:javascript复制module.exports = function npmRunAll(args, stdout, stderr) {
try {
const stdin = process.stdin
const argv = parseCLIArgs(args)
} catch () {
// ...
}
}
解析处理所有标准输入流参数,最终生成并返回 ArgumentSet 实例 set。parseCLIArgsCore 只看控制任务流执行的参数:
代码语言:javascript复制function addGroup(groups, initialValues) {
groups.push(Object.assign(
{ parallel: false, patterns: [] },
initialValues || {}
))
}
function parseCLIArgsCore(set, args) {
LOOP:
for (let i = 0; i < args.length; i) {
const arg = args[i]
switch (arg) {
// ...
case "-s":
case "--sequential":
case "--serial":
if (set.singleMode && arg === "-s") {
set.silent = true
break
}
if (set.singleMode) {
throw new Error(`Invalid Option: ${arg}`)
}
addGroup(set.groups)
break
case "-p":
case "--parallel":
if (set.singleMode) {
throw new Error(`Invalid Option: ${arg}`)
}
addGroup(set.groups, { parallel: true })
break
default: {
// ...
break
}
}
}
// ...
return set
}
将任务都装到 groups
数组中,如果是并行任务(传了 -p
、--parallel
参数),就给任务加上 { parallel: true }
标记。默认是 { parallel: false }
,即串行任务。
执行任务组
在进入这一小节之前,我们就 npm-run-all 源码在 scripts 下加一条 debug 命令:
代码语言:javascript复制$ "node ./bin/npm-run-all/index.js lint test"
解析完参数生成的 argv.groups
如下:
[{
paralles: false,
patterns: ['lint', 'test']
}]
有了这个数组结果,我们再看任务执行流程会更加明朗。
代码语言:javascript复制// bin/npm-run-all/main.js
module.exports = function npmRunAll(args, stdout, stderr) {
try {
// 省略解析参数
// 执行任务
const promise = argv.groups.reduce(
(prev, group) => {
// 分组中没有任务,直接返回 null
if (group.patterns.length === 0) {
return prev
}
return prev.then(() => runAll(
group.patterns, // ['lint', 'test']
{
// ……
// 是否并行执行
parallel: group.parallel,
// 并行的最大数量
maxParallel: group.parallel ? argv.maxParallel : 1,
// 一个任务失败后继续执行其他任务
// ……
arguments: argv.rest,
// 这个