1. 前文回顾
上文是 webpack 启动过程有关 webpack-cli 这个模块的实现部分,本文主要做了以下工作:
- 有关
npm注册webpack bin命令的过程------package.json.bin字段,全局安装时可以全局调用; webpack-cli提供的命令类型------内建命令+外置内建命令;WebpackCLI构造函数的工作 ------ 通过Commander初始化命令解析程序this.program;- 最走进入到
WebpackCLI的入口方法WebpackCLI.prototype.run方法,主要用于解析操作命令及操作数; - 判断
commandToRun是否为已知命令; - 处理未知命令:特殊情况
entry语法,否则为错误命令; - 调用
program.parseAsyanc执行新创建的命令; - 我们还讨论另一个重要方法
makeCommand方法,该方法为新生成的命令注册 actionHanlder 函数,这里这个 actionHandler 才是真正处理用户意图的命令;
2.1 整体流程核心设计
下图是一张 webpack-cli 的整体工作流程:

整体来看 webpack-cli 通过以下过程实现整个 webpack-cli 工作:
- 接收从
webpack命令的整体输入process.argv - 解析
process.argv得到operands和options - 假设
options是help/version语法并处理,即--help/-h或者--version/-v; - 如果不是
help/version的options语法则注册actioon handler,在action handler主要生成子命令并执行,主要包含以下过程: - 判断是否为
help/version命令语法,即webpack help/version - 调用
loadCommandByName通过名字加载并调用this.makeCommand注册子命令或者加载外置的命令再注册,最终得到生成的子命令 - 调用
this.program.parseAsync(\[子命令])执行新生成的子命令 - 调用
this.program.parseAsync(process.argv)解析外部输入
3. 对比 V3
对比 V3,V5 在架构层面做了以下升级: 从 optiosn 向子命令转型,V3 中以 --flgs 为主,V5 则是子命令; 剥离与常用构建无关的命令,即 externalBuiltInCommand 的实现; 转用生成式的 CLI 命令创建和执行; 那这里最值得玩味的是最后两点,现在大家开始思考为什么这么做?
4. 为什么这么做?
在看这些代码的时候,有两点最让人困惑,即上面的两次调用 this.program.parseAsync 方法,常规的 CLI 命令行中一般只有一次这个动作,为什么在 webpack-cli 中有两次呢?
这个问题也是解答 webpack 为什么做了这么多的代码,区区几个命令,为啥要写上几百行代码?我一直困惑,知道看完所有的细节才拍大腿的设计------我怎么就没想到呢?
4.1 外置内建命令设计
要回答为什么的问题首先需要回忆大家日常最常用的命令:
webpack或(webpack watch、webpack build)
webpack help或(webpack -h)webpack version或者(webpack -v)
有没有发现,这些命令刚好是 webpack 的内建且内置的命令,其余的都拆分到了其他 npm 包里面。
这个思路和我们常见的优化较大规模应用加载性能的思路一样------懒加载!
其核心是根据用户使用场景,用的多的命令内建内置,所见即所得的体验。其他不常用的命令不用不安装、不加载,待用时则自动安装!
4.2 生成式命令行设计
说完了 externalBuiltInCommands 设计的优势,再看第二点生成式 CLI 的好处是什么。
先来看一个声明式的命令行工具:
js
import { Command } from 'commander';
const program = new Command();
program.version(require('../package').version)
.usage('<command> [options]')
.command('q', 'get challenge sms code from didifarm')
.command('add', 'add some alias:phone to config.json')
.command('cddfarm', 'modify didifarm config')
.command('ls', 'list cfg.json in ~ dir')
.command('cp', 'cp an phone of an alias')
.parse(process.argv)
这个命令行工具是我们内部实现的一个验证码查询工具:joymax
这个工具在命令行启动的一瞬间会把这个命令实现的 q、add、cddfarm、ls、cp 所有命令的相关模块全部加载。
webpack 的生成式命令行伪代码可以简化成以下的条件语句:
js
// 获取用户输出 npx webpack build
const { operands, options } = this.program.parseOptions(process.argv);
if (operands === helpSyntax) {
// 注册 help 命令
this.makeCommand(helpOps)
} else if (operands === versionSyntax) {
// 注册 version 命令
this.makeCommand(versionOps)
} else if (operands === buildSyntax) {
// 注册 build/watch 命令
this.makeCommand(buildWathcOpts)
} else if (...) {
this.makeCommand(.....)
}
这样我们会发现 webpack-cli 内部 只注册当前输入要执行的命令 ,这样也就只加载这 1 个被注册命令相关模块,其余的命令 webpack 并不注册,模块也就不会被加载!
通过上面的分析比对我们可以发现,整个设计都是在尽可能减少命令行启动时需要加载的模块数量!
这种设计的优势也是不言而喻的,通过直接减少运行加载的模块数量降低运行时内存开销,提升 webpack-cli 运行时性能!
5. 总结
好了,到这里 webpack-cli 及 webpack 这个命令行工具的实现过程就已经全部讲完了,这一篇算是个总结和收尾之作,最后来回归一下 webpack 到底是怎么实现的以及这么做的好处;
首先,webpack 命令是由 webpack 这个包注册到 node_modules/.bin/webpack 中的。当然这是个软链接,链接到 node_modules/webpack/bin/webpack.js 脚本,但是 webpack 后续的命令,比如 build,都是由 webpack-cli 包实现的。
在 node_modules/webpack/bin/webpack.js 这个脚本中,我们学习了通过 fs.statSync/require.resolve 这两个方式进行 webpack-cli 的安装检测;另外,当没有安装的时候,我们还学习了通过 readline 创建 REPL 对用户引导,当用户同意后,我们还学习了使用 child_process 执行安装命令自动进行包的安装;最后则是通过 require(webpack-cli) 的方式执行 webpack-cli。
在学习 webpack-cli 即 node_modules/webpack-cli/bin/cli.js 模块,这个模块内部导入了 webpack-cli/lib/bootstrap.js 模块;
在 bootstrap.js 中,基于 lib/webpack-cli.js 模块导出的 WebpackCLI 类型完成 CLI 实例的创建;在这个过程中我们学习了以下思想:
- 懒加载思想,首先根据某种规则把
webpack-cli的命令分成"内建内置"和"内建外置"两种命令,其中内建内置也就是开箱即用的:build/watch/version/help四个,剩余的命令拆分到其他包中,用到时再行安装;这么做可以有效缩减开发包的体积,运行时可以进一步轻量化; - 用时注册,这一步相当于是进一步优化,上面说了,即便内建内置,仍然存在
4个命令,但是同一时空下只能执行其中一个,剩下的三个是无用的,因此,webpack-cli采用了更极端的方式------运行时生成命令行,运行哪个注册哪个。
webpack-cli 通过这两种方式极限压缩了 webpack-cli 对启动阶段的资源消耗情况,为后续的 compiler 工作减轻了资源压力。
这种通盘考虑,多管齐下的思路值得我们借鉴,某事当谋全局!