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
工作减轻了资源压力。
这种通盘考虑,多管齐下的思路值得我们借鉴,某事当谋全局!