作者:马宾
1. 背景
前一段时间偶然间注意到伴随着 webpack 版本从 V4 升级到 V5, webpack-cli 也发生了大版本升级,但当时重心不是 webpack-cli 所以只是草草看了一下。
恰逢组内有一场有关 CLI 的讨论,我想到了 webpack-cli 的升级,随着深入发现 webpack-cli 经历了 V3 - V5 的升级,这些升级给我带来了不小的惊喜,主要体现在两方面:
- 生成式的 CLI 实现方式;
- 懒加载思想在 CLI 上的实践;
webpack-cli 从 V3 升级到 V5 中间有一个过渡版本 V4,V5 主要做了限制 webpack 及其依赖包的最低版本号等工作,目前 webpack-cli V5 仅支持 5.0 以上的 webpack。整体实现上,与 V4 与 V5 没有特性层面的升级,本文以 V5 代码为例进行讨论。
2. webpack-cli 安装及使用
2.1 webpack-cli 安装
- 执行安装命令
npm install -D webpack webpack-cli
- bin 命令注册
webpack/webpack-cli npm 包的 package.json 中包含以下 bin 字段:
"bin": {
"webpack-cli": "bin/cli.js"
},
在本地安装时,执行 npm install 的过程中会将 bin 对应的可执行文件软链接到 **node_modules/.bin**
目录中,作为一个可以通过以下两种方式之一调用的命令:
**package.json.scrips + npm run**
**npx**
2.2 用法
# 用法
npx webpack watch [options]
# 示例
npx webpack watch --mode development
3. webpack 实现
webpack 命令行由 node_modules/.bin/webpack 实现(下称 webpack),该可执行文件是 webpack/bin/webpack.js 的软链。
3.1 工作原理
webpack/bin/webpack.js 内部依赖 webpack-cli 包(下称 webpack-cli),webpack 并不负责具体的 CLI 命令的实现和处理等具体的工作,具体工作均由 webpack-cli 实现。
webpack 主要就 webpack-cli 的安装情况进行检测,如安装了则执行调用 webpack-cli ,若未安装则进行安装引导,用户同意后进行安装,安装结束后再调用 webpack-cli 执行具体的动作。
webpack 命令工作原理如下图:
3.2 实现细节
根据上面的工作原理图,现就图中的各个步骤的实现细节进行详细讨论。
3.2.1 webpack-cli 包安装检测
首先,脚本内创建 cli 对象,存储 webpack-cli 包括安装情况在内的信息:
const cli = {
name: "webpack-cli",
package: "webpack-cli",
binName: "webpack-cli",
installed: isInstalled("webpack-cli"), // 安装情况
url: "https://github.com/webpack/webpack-cli"
};
webpack 脚本内部采用 isInstalled 方法检查安装情况,其原理是:
fs.statSync 获取 stat 对象,再通过 stat.isDierectory() 判断 webpack-cli 目录是否存在:
const isInstalled = packageName => {
if (process.versions.pnp) {
return true;
}
const path = require("path");
const fs = require("graceful-fs");
let dir = __dirname;
do {
try {
if (
fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()
) {
return true;
}
} catch (_error) {
// Nothing
}
} while (dir !== (dir = path.dirname(dir)));
return false;
};
while 循环从 node_modules/webpack/bin 下面这个目录开始逐级的向上查找,一直找到根目录下的 node_mdules 的过程:
while (dir !== (dir = path.dirname(dir)))
dir 的初始值是 webpack 脚本文件所在目录,即 /user/didi/Document/Proj/node_modules/webpack/bin
,通过 dir = path.dirname(dir) 逐级找到 /
根目录,以下即为查找的最长链路:
- /user/didi/Document/Proj/node_modules/webpack/bin
- /user/didi/Document/Proj/node_modules/webpack
- /user/didi/Document/Proj/node_modules
- /user/didi/Document/Proj
- /user/didi/
- /user
- /
当找到 /
时,path.dirname('/') == '/' 返回 true,此时相当于查找到了根目录,如果没有找到则认定没有。
之所以有找到全局这个动作,是因为 webpack-cli 支持全局安装,因此查找路径到需要到根目录。这里其实对应的背后是 Node.js 查找依赖包的规则。
3.2.2 处理安装检测结果
根据 cli.installed 属性得出 webpack-cli 安装情况,若安装则调用 cli,未安装引导安装;
if (!cli.installed) {
// 引导安装
} else {
// 调用
}
3.2.2.1 已经安装
已经安装调用 runCli 方法并传入 cli 对象;
runCli(cli);
runCli 方法是 webpack 脚本内部提供的加载 webpack-cli 模块的方法,其核心实现就是 require webpack-cli 模块:
const runCli = cli => {
// ....
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};
3.2.2.2 未安装
当检测所得结果是 webpack-cli 尚未安装时,webpack 会引导用户进行安装,具体工作如下:
- 包管理器检查
根据 yarn.lock 判定 yarn,根据 pnpm-lock.yaml 判定 pnpm ,否则兜底采用 npm;
let packageManager;
if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {
packageManager = "yarn";
} else if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {
packageManager = "pnpm";
} else {
packageManager = "npm";
}
- 创建 REPL 接口
webpack 通过 Node.js 提供的 readline 模块完成交互式命令行界面,传送门readline
const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr
});
- REPL 询问用户并处理用户输入
下面会询问用户是否打算安装 webpack
const question = `Do you want to install 'webpack-cli' (yes/no): `;
发起询问通过 questionInterface.question 方法:
// 询问
questionInterface.question(question, answer => {
// 这个回调用户接受用户输入
});
- 处理用户输入
如果用户输入不是 y/Y 打头的,说明用户不打算安装,打印错误信息后终止进程!
if (!normalizedAnswer) {
console.error(
"You need to install 'webpack-cli' to use webpack via CLI.\n" +
"You can also install the CLI manually."
);
return;
}
process.exitCode = 0;
如果是 y/Y 开头的,认定用户输入需要安装,此时调用 runCommand 方法,结合上面的包管理器检测结论进行自动安装 webpack-cli;
runCommand(packageManager, installOptions.concat(cli.package))
runCommand 内部使用 Node.js 原生模块 child_process 创建子进程共享当前管道进行安装:
const runCommand = (command, args) => {
const cp = require("child_process");
return new Promise((resolve, reject) => {
const executedCommand = cp.spawn(command, args, {
stdio: "inherit",
shell: true
});
executedCommand.on("error", error => {
reject(error);
});
executedCommand.on("exit", code => {
if (code === 0) {
resolve();
} else {
reject();
}
});
});
}
成功安装后同样采用 runCli 调用 webpack-cli 处理 webpack 的子命令。
runCommand(packageManager, installOptions.concat(cli.package))
.then(() => {
runCli(cli);
})
4. webpack-cli 实现
结合上面的 webpack 实现中可以得知,webpack 脚本最后通过 runCli 方法 加载 webpack-cli/bin/cli.js。
下面我们来看 webpack-cli 工作流程:
4.1 webpack-cli/bin/cli.js
cli.js 内部就做了一件事,导入 ../lib/bootstrap 模块,并且执行该模块的导入传入 process.argv 进程参数;
#!/usr/bin/env node
const runCLI = require("../lib/bootstrap");
process.title = "webpack";
runCLI(process.argv)
4.2 webpack-cli/lib/bootstrap.js
该模块导出了一个函数 runCLI:
const WebpackCLI = require("./webpack-cli");
const runCLI = async (args) => {
const cli = new WebpackCLI();
try {
await cli.run(args);
}
catch (error) {
}
};
module.exports = runCLI;
runCLI 函数内部创建 WebpackCLI 实例 cli,然后调用 cli.run() 方法。run 方法是 WebpackCLI 类型的入口方法。
4.3 webpack-cli/lib/webpack-cli.js
该模块是整个 webpack CLI 界面实现核心部分,这个类型使用 comamnder 包在运行时解析用户输入创建并执行相应命令。
class WebpackCLI {
constructor () {}
run (args, parsOptions) {}
}
module.exports = WebpackCLI
webpack-cli 内置了以下四个命令,这些命令开箱即用:
- build (default):运行 webpack(默认命令,可用输出文件)
- watch:运行 webpack 并且监听文件变化
- version:显示已安装的 package 以及子 package 的版本
- help:列出命令行可用的基础命令和 flag
此外,剩下的命令,webpack-cli 做了“特殊处理”,即 webpack-cli 里面的 "exteralBuildInCommands" 即 【外置内建命令】,这些命令包括:
- serve:运行 webpack 开发服务器
- info:输出你的系统信息
- init:用于初始化一个新的 webpack 项目
- loader:初始化一个 loader
- plugin:初始化一个插件
- migrate:这个命令文档未列出[npm]
- configtest:校验 webpack 配置
以上命令在 webpack-cli 内部称为 "knownCommands "【已知命令】
4.3.1 contructor
构造函数内部通过 commander 创建了 program 对象并挂在到 WebpackCLI 实例之上:
constructor() {
this.colors = this.createColors();
this.logger = this.getLogger();
// Initialize program
this.program = program;
this.program.name("webpack");
this.program.configureOutput({
writeErr: this.logger.error,
outputError: (str, write) => write(`Error: ${this.capitalizeFirstLetter(str.replace(/^error:/, "").trim())}`),
});
}
4.3.2 run 方法
run 方法是 WebpackCLI 的主入口
- exitOverride 改写退出
这是由于 comander 在声明式的命令行有一些默认的退出机制,比如没有找到命令等情况,但是在 webpack-cli 这种生成式的 CLI 中,有些命令可能是运行时生成的,所以不能直接退出,需要做一些拦截动作,然后自定义退出过程。
this.program.exitOverride(async (error) => {....})
- 注册 color/no-color options
this.program.option("--color", "Enable colors on console.");
this.program.on("option:color", function () {
// @ts-expect-error shadowing 'this' is intended
const { color } = this.opts();
cli.isColorSupportChanged = color;
cli.colors = cli.createColors(color);
});
this.program.option("--no-color", "Disable colors on console.");
this.program.on("option:no-color", function () {
// @ts-expect-error shadowing 'this' is intended
const { color } = this.opts();
cli.isColorSupportChanged = color;
cli.colors = cli.createColors(color);
});
让 webpack 命令行的输出五颜六色的,这些不作为重点讨论!
- 注册 version option
const outputVersion = async (options) => {})
this.program.option("-v, --version", "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.");
这里有个 outputVersion 方法,该方法内部输出 webpack 包的版本信息;
- 处理 help option
注意,webpack-cli 同样静默了 commander 默认的 help 命令支持;
.addHelpCommand()
A help command is added by default if your command has subcommands. You can explicitly turn on or off the implicit help command with .addHelpCommand() and .addHelpCommand(false).
this.program.helpOption(false);
this.program.addHelpCommand(false);
this.program.option("-h, --help [verbose]", "Display help for commands and options.");
在稍后的生成式命令中,webpack-cli 自己处理 help 命令具体动作。
- 注册 action handler
this.program.action(async (options, program) => {})
action handler 是 webpack-cli 生成式 CLI 的大脑,在 action handler 内部主要做了以下工作:
5.1 解析进程参数获取 operands, options
// Command and options
const { operands, unknown } = this.program.parseOptions(program.args);
const defaultCommandToRun = getCommandName(buildCommandOptions.name);
const hasOperand = typeof operands[0] !== "undefined";
const operand = hasOperand ? operands[0] : defaultCommandToRun;
5.2 判断是否是 help
判断如果是 --help 及相关 help 语法则调用前文注册的 outputHelp 方法输出帮助信息!
const isHelpOption = typeof options.help !== "undefined";
const isHelpCommandSyntax = isCommand(operand, helpCommandOptions);
if (isHelpOption || isHelpCommandSyntax) {
await outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, program);
}
5.3 判断是否是 version
与上面 help 类似,如果是 --version 类似语法,则输出 version 相关信息
const isVersionOption = typeof options.version !== "undefined";
const isVersionCommandSyntax = isCommand(operand, versionCommandOptions);
if (isVersionOption || isVersionCommandSyntax) {
await outputVersion(optionsForVersion);
}
5.4 处理非 help 或 version 的语法
let commandToRun = operand;
let commandOperands = operands.slice(1);
operand 在前面判断过,如果没有传递则默认使用 build 命令:
const defaultCommandToRun = getCommandName(buildCommandOptions.name);
const operand = hasOperand ? operands[0] : defaultCommandToRun;
commandOperands 则是webpack 子命令的操作数;
5.5 判断 commandToRun 是否为已知命令
所谓已知命令就是前面提到的 "knownCommands",如果是则直接进行加载并执行的的动作
if (isKnownCommand(commandToRun)) {
await loadCommandByName(commandToRun, true);
}
loadingCommandByName 方法用于加载并创建命令,然后执行执行命令,该方法将外部传入的已知命名分为以下四种情况处理:
- commandToRun 是 build 或者 watch 命令
- commandToRun 是 help 命令
- commandToRun 是 version 命令
- commandToRun 是 externalBuiltIn 命令
const loadCommandByName = async (commandName, allowToInstall = false) => {
const isBuildCommandUsed = isCommand(commandName, buildCommandOptions);
const isWatchCommandUsed = isCommand(commandName, watchCommandOptions);
if (isBuildCommandUsed || isWatchCommandUsed) {
// 处理 webpack build/watch
}
else if (isCommand(commandName, helpCommandOptions)) {
// 处理 help
this.makeCommand(helpCommandOptions, [], () => { });
}
else if (isCommand(commandName, versionCommandOptions)) {
// 处理 version
this.makeCommand(versionCommandOptions, [], () => { });
}
else {
// 处理 externalBuiltInCommand loading
}
};
前三种直接调用 WebpackCLI.prototype.makeCommand 创建本次要运行的子命令(详情见下面 4.3.3 makeCommand);makeCommand 结束后,需要运行的命令就生成,静待触发。
这里以 webpack build/watch 为例看下:
this.makeCommand(isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, async () => {
// 这个函数是 webpack 运行所支持的 options
this.webpack = await this.loadWebpack(); // 加载 webpack 包
return isWatchCommandUsed
? this.getBuiltInOptions().filter((option) => option.name !== "watch")
: this.getBuiltInOptions();
}, async (entries, options) => {
// 这个就是 webpack build/watch 的 action handler 函数
// 当用户执行 npx webpack watch 就会执行这个命令
if (entries.length > 0) {
options.entry = [...entries, ...(options.entry || [])];
}
await this.runWebpack(options, isWatchCommandUsed);
});
除了上面的三种情况快,最后一种就是 externalBuiltInCommand 加载过程。这个名字着实让人很迷惑,为什么叫“外置”,还“内建”?
所谓外置,是因为 webpack-cli 这个包只包含了基础的 build/watch/help/version 的实现,剩下命令的实现被拆分到了其他的包中。
对于webpack-cli 来说,命令确实是支持了不额外扩展,这所谓“内建”,但是真真正正实现这个命令的脚本文件在另一个依赖包中,这就是“外置”。
const builtInExternalCommandInfo = externalBuiltInCommandsInfo.find((externalBuiltInCommandInfo) => getCommandName(externalBuiltInCommandInfo.name) === commandName ||
(Array.isArray(externalBuiltInCommandInfo.alias)
? externalBuiltInCommandInfo.alias.includes(commandName)
: externalBuiltInCommandInfo.alias === commandName));
let pkg;
if (builtInExternalCommandInfo) {
({ pkg } = builtInExternalCommandInfo);
}
else {
pkg = commandName;
}
if (pkg !== "webpack-cli" && !this.checkPackageExists(pkg)) {
if (!allowToInstall) {
return;
}
pkg = await this.doInstall(pkg, {
preMessage: () => {
this.logger.error(`For using this command you need to install: '${this.colors.green(pkg)}' package.`);
},
});
}
let loadedCommand;
try {
loadedCommand = await this.tryRequireThenImport(pkg, false);
}
catch (error) {
// Ignore, command is not installed
return;
}
let command;
try {
command = new loadedCommand();
await command.apply(this);
}
catch (error) {
this.logger.error(`Unable to load '${pkg}' command`);
this.logger.error(error);
process.exit(2);
}
}
5.6 处理未知命令
前文中介绍过 webpack 内置了 11 个命令,除此之外的都算作未知命令。处理未知命令有两种情况:
5.6.1 entry 语法
webpack CLI 支持 entry 语法:
$ npx webpack <entry> --output-path <output-path>
处理 webpack enry 语法时首先检测传入的 入口文件是否存在,若存在则按照webpack 的默认命令 build 进行加载。
5.6.2 错误命令
如果是未知命令切不是入口语法的情况下,webpack CLI 认定我们的输入有误,CLI 此时会尝试查找与输入单词最接近的命令并提示到命令行;
webpack-cli 内部使用 fastest-levenshtein 找到与输入最接近的命令;
this.logger.error(`Unknown command or entry '${operand}'`);
const levenshtein = require("fastest-levenshtein"); // 这个库用于计算两个词之间的差别
const found = knownCommands.find((commandOptions) => levenshtein.distance(operand, getCommandName(commandOptions.name)) < 3);
if (found) {
this.logger.error(`Did you mean '${getCommandName(found.name)}' (alias '${Array.isArray(found.alias) ? found.alias.join(", ") : found.alias}')?`);
}
this.logger.error("Run 'webpack --help' to see available commands and options");
process.exit(2);
5.7 调用 program.parseAsyanc 执行新创建的命令
await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], {
from: "user",
});
下面我们简单了解一下 webpack-cli 用于动态生成命令行的方法 makeCommand 以及 cli 内部自动安装的 doInstall 方法!
4.3.3 makeCommand
- 签名:
- commandOptions:创建命令所需 option
- options:命令执行所需 options
- action:处理命令的 action handler
1.1 函数工作流
- 判断是否是已经加载过的命令,若已加载则不再 make
- 调用 program.comman() 注册新的子命令
- 注册 command.description() 描述信息
- 注册 command.usage() 用法信息
- 注册 command.alias() 别名信息
- 检查命令的依赖包的安装信息
- 为新增的 command 注册传入的 options
- 最后为新 command 注册 aciton handler
async makeCommand(commandOptions, options, action) {
// 校验是否已经 make 过
const alreadyLoaded = this.program.commands.find((command) => command.name() === commandOptions.name.split(" ")[0] ||
command.aliases().includes(commandOptions.alias));
if (alreadyLoaded) {
// 已经 make 过了终止
return;
}
// 注册新命令
const command = this.program.command(commandOptions.name, {
noHelp: commandOptions.noHelp,
hidden: commandOptions.hidden,
isDefault: commandOptions.isDefault,
});
// 注册 options
if (commandOptions.description) {
command.description(commandOptions.description, commandOptions.argsDescription);
}
// 注册 usase
if (commandOptions.usage) {
command.usage(commandOptions.usage);
}
// 注册别名
if (Array.isArray(commandOptions.alias)) {
command.aliases(commandOptions.alias);
}
else {
command.alias(commandOptions.alias);
}
if (commandOptions.pkg) {
command.pkg = commandOptions.pkg;
}
else {
command.pkg = "webpack-cli";
}
const { forHelp } = this.program;
let allDependenciesInstalled = true;
// 检查依赖并安装缺失依赖
if (commandOptions.dependencies && commandOptions.dependencies.length > 0) {
for (const dependency of commandOptions.dependencies) {
// 校验依赖是否在 webpack 目录下存在
const isPkgExist = this.checkPackageExists(dependency);
if (isPkgExist) {
continue;
}
else if (!isPkgExist && forHelp) {
allDependenciesInstalled = false;
continue;
}
let skipInstallation = false;
// Allow to use `./path/to/webpack.js` outside `node_modules`
if (dependency === WEBPACK_PACKAGE && fs.existsSync(WEBPACK_PACKAGE)) {
skipInstallation = true;
}
// Allow to use `./path/to/webpack-dev-server.js` outside `node_modules`
if (dependency === WEBPACK_DEV_SERVER_PACKAGE && fs.existsSync(WEBPACK_PACKAGE)) {
skipInstallation = true;
}
if (skipInstallation) {
continue;
}
// 自动安装缺失的依赖
await this.doInstall(dependency, {
preMessage: () => { 输出警告信息 },
});
}
}
if (options) {
// 注册 options
}
// 注册 action handler
command.action(action);
return command
4.3.4 doInstall
doInstall 方法和前面的 webpack 脚本引导安装 webpack-cli 的实现思路异曲同工,不再展开,大致工作如下:
- 获取包管理器
- 创建 REPL 引导用户输入
- 创建子进程执行安装命令
async doInstall(packageName, options = {}) {
// 获取包管理器i
const packageManager = this.getDefaultPackageManager();
if (!packageManager) {
this.logger.error("Can't find package manager");
process.exit(2);
}
if (options.preMessage) {
options.preMessage();
}
// 创建 REPL
const prompt = ({ message, defaultResponse, stream }) => {
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: stream,
});
return new Promise((resolve) => {
rl.question(`${message} `, (answer) => {
// Close the stream
rl.close();
const response = (answer || defaultResponse).toLowerCase();
// Resolve with the input response
if (response === "y" || response === "yes") {
resolve(true);
}
else {
resolve(false);
}
});
});
};
// yarn uses 'add' command, rest npm and pnpm both use 'install'
const commandArguments = [packageManager === "yarn" ? "add" : "install", "-D", packageName];
const commandToBeRun = `${packageManager} ${commandArguments.join(" ")}`;
let needInstall;
try {
needInstall = await prompt({
message: `[webpack-cli] Would you like to install '${this.colors.green(packageName)}' package? (That will run '${this.colors.green(commandToBeRun)}') (${this.colors.yellow("Y/n")})`,
defaultResponse: "Y",
stream: process.stderr,
});
}
catch (error) {
this.logger.error(error);
process.exit(error);
}
if (needInstall) {
// 子进程执行安装命令
const { sync } = require("cross-spawn");
try {
sync(packageManager, commandArguments, { stdio: "inherit" });
}
catch (error) {
this.logger.error(error);
process.exit(2);
}
return packageName;
}
process.exit(2);
}
5. 总结
5.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) 解析外部输入
5.2 对比 V3
对比 V3,V5 在架构层面做了以下升级:
- 从 optiosn 向子命令转型,V3 中以 --flgs 为主,V5则是子命令;
- 剥离与常用构建无关的命令,即 externalBuiltInCommand 的实现;
- 转用生成式的CLI 命令创建和执行;
那这里最值得玩味的是最后两点,现在大家开始思考为什么这么做?
5.3 为什么这么做?
5.3.1 外置内建命令设计
要回答为什么的问题首先需要回忆大家日常最常用的命令:
- webpack 或(webpack watch、webpack build)
- webpack help 或(webpack -h)
- webpack version 或者(webpack -v)
有没有发现,这些命令刚好是 webpack 的内建且内置的命令,其余的都拆分到了其他 npm 包里面。
这个思路和我们常见的优化较大规模应用加载性能的思路一样——懒加载!
其核心是根据用户使用场景,用的多的命令内建内置,所见即所得的体验。其他不常用的命令不用不安装、不加载,待用时则自动安装!
5.3.2 生成式命令行设计
说完了 externalBuiltInCommands 设计的优势,再看第二点生成式 CLI 的好处是什么。
先来看一个声明式的命令行工具:
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)
这个命令行工具是我们内部实现的一个查询工具
这个工具在命令行启动的一瞬间会把这个命令实现的 q、add、cddfarm、ls、cp 所有命令的相关模块全部加载。
webpack 的生成式命令行伪代码可以简化成以下的条件语句:
// 获取用户输出 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 运行时性能!