我读 webpack-cli 源码得到的 CLI 优化启示

1. 前文回顾

上文是 webpack 启动过程有关 webpack-cli 这个模块的实现部分,本文主要做了以下工作:

  1. 有关 npm 注册 webpack bin 命令的过程------package.json.bin 字段,全局安装时可以全局调用;
  2. webpack-cli 提供的命令类型------内建命令+外置内建命令;
  3. WebpackCLI 构造函数的工作 ------ 通过 Commander 初始化命令解析程序 this.program
  4. 最走进入到 WebpackCLI 的入口方法 WebpackCLI.prototype.run 方法,主要用于解析操作命令及操作数;
  5. 判断 commandToRun 是否为已知命令;
  6. 处理未知命令:特殊情况 entry 语法,否则为错误命令;
  7. 调用 program.parseAsyanc 执行新创建的命令;
  8. 我们还讨论另一个重要方法 makeCommand 方法,该方法为新生成的命令注册 actionHanlder 函数,这里这个 actionHandler 才是真正处理用户意图的命令;

2.1 整体流程核心设计

下图是一张 webpack-cli 的整体工作流程:

整体来看 webpack-cli 通过以下过程实现整个 webpack-cli 工作:

  1. 接收从 webpack 命令的整体输入 process.argv
  2. 解析 process.argv 得到 operandsoptions
  3. 假设 optionshelp/version 语法并处理,即 --help/-h 或者 --version/-v
  4. 如果不是 help/versionoptions 语法则注册 actioon handler,在 action handler 主要生成子命令并执行,主要包含以下过程:
  5. 判断是否为 help/version 命令语法,即 webpack help/version
  6. 调用 loadCommandByName 通过名字加载并调用 this.makeCommand 注册子命令或者加载外置的命令再注册,最终得到生成的子命令
  7. 调用 this.program.parseAsync(\[子命令]) 执行新生成的子命令
  8. 调用 this.program.parseAsync(process.argv) 解析外部输入

3. 对比 V3

对比 V3V5 在架构层面做了以下升级: 从 optiosn 向子命令转型,V3 中以 --flgs 为主,V5 则是子命令; 剥离与常用构建无关的命令,即 externalBuiltInCommand 的实现; 转用生成式的 CLI 命令创建和执行; 那这里最值得玩味的是最后两点,现在大家开始思考为什么这么做?

4. 为什么这么做?

在看这些代码的时候,有两点最让人困惑,即上面的两次调用 this.program.parseAsync 方法,常规的 CLI 命令行中一般只有一次这个动作,为什么在 webpack-cli 中有两次呢?

这个问题也是解答 webpack 为什么做了这么多的代码,区区几个命令,为啥要写上几百行代码?我一直困惑,知道看完所有的细节才拍大腿的设计------我怎么就没想到呢?

4.1 外置内建命令设计

要回答为什么的问题首先需要回忆大家日常最常用的命令:

  • webpack 或(webpack watchwebpack 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-cliwebpack 这个命令行工具的实现过程就已经全部讲完了,这一篇算是个总结和收尾之作,最后来回归一下 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-clinode_modules/webpack-cli/bin/cli.js 模块,这个模块内部导入了 webpack-cli/lib/bootstrap.js 模块;

bootstrap.js 中,基于 lib/webpack-cli.js 模块导出的 WebpackCLI 类型完成 CLI 实例的创建;在这个过程中我们学习了以下思想:

  1. 懒加载思想,首先根据某种规则把 webpack-cli 的命令分成 "内建内置""内建外置" 两种命令,其中 内建内置 也就是开箱即用的:build/watch/version/help 四个,剩余的命令拆分到其他包中,用到时再行安装;这么做可以有效缩减开发包的体积,运行时可以进一步轻量化;
  2. 用时注册,这一步相当于是进一步优化,上面说了,即便内建内置,仍然存在 4 个命令,但是同一时空下只能执行其中一个,剩下的三个是无用的,因此,webpack-cli 采用了更极端的方式------运行时生成命令行,运行哪个注册哪个。

webpack-cli 通过这两种方式极限压缩了 webpack-cli 对启动阶段的资源消耗情况,为后续的 compiler 工作减轻了资源压力。

这种通盘考虑,多管齐下的思路值得我们借鉴,某事当谋全局!

相关推荐
小镇程序员11 分钟前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐13 分钟前
前端图像处理(一)
前端
程序猿阿伟21 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒22 分钟前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪31 分钟前
AJAX的基本使用
前端·javascript·ajax
力透键背34 分钟前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M1 小时前
node.js第三方Express 框架
前端·javascript·node.js·express
盛夏绽放1 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
想自律的露西西★1 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5
白墨阳1 小时前
vue3:瀑布流
前端·javascript·vue.js