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 安装
- 执行安装命令
shell
npm install -D webpack webpack-cli
- bin 命令注册
webpack/webpack-cli npm 包的 package.json 中包含以下 bin 字段:
json
"bin": {
"webpack-cli": "bin/cli.js"
},
在本地安装时,执行 npm install 的过程中会将 bin 对应的可执行文件软链接到 node_modules/.bin
目录中,作为一个可以通过以下两种方式之一调用的命令:
package.json.scrips + npm run
npx
2.2 用法
- 用法
shell
npx webpack watch [options]
- 示例
shell
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 包括安装情况在内的信息:
js
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 目录是否存在:
js
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 的过程:
js
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,未安装引导安装;
js
if (!cli.installed) {
// 引导安装
} else {
// 调用
}
3.2.2.1 已经安装
已经安装调用 runCli 方法并传入 cli 对象;
js
runCli(cli);
runCli 方法是 webpack 脚本内部提供的加载 webpack-cli 模块的方法,其核心实现就是 require webpack-cli 模块:
js
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;
js
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
js
const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr
});
- REPL 询问用户并处理用户输入
下面会询问用户是否打算安装 webpack
js
const question = `Do you want to install 'webpack-cli' (yes/no): `;
发起询问通过 questionInterface.question 方法:
js
// 询问
questionInterface.question(question, answer => {
// 这个回调用户接受用户输入
});
- 处理用户输入
如果用户输入不是 y/Y 打头的,说明用户不打算安装,打印错误信息后终止进程!
js
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;
js
runCommand(packageManager, installOptions.concat(cli.package))
runCommand 内部使用 Node.js 原生模块 child_process 创建子进程共享当前管道进行安装:
js
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 的子命令。
js
runCommand(packageManager, installOptions.concat(cli.package))
.then(() => {
runCli(cli);
})
4. 总结
这是 webpack 源码部分的第一个环节,webpack-cli 的启动,webpack 相当于是一把钥匙,用来启动 webpack 这辆大车的钥匙;
本文详细讨论了 webpack-cli 相关作用及部分实现,主要包含以下内容:
- webpack-cli 的安装及用法;
- webpack-cli 中的实现模块 webpack.js 的实现原理:
- 2.1 通过 isInstalled 方法检测 webpack-cli 安装情况;
- 2.2 针对安装情况做出不同处理:
- 2.2.1 已安装时则直接调用 webpack-cli;
- 2.2.2 未安装时则有针对性的进行引导,具体的实现重点讲述了
- 1)包管理的检测,如 npm/yarn 等;
- 2)通过 readline 模块创建 创建 REPL 接口征询用户意见;
- 3)用户同意安装后通过创建子进程的方式调用安装命令;
- 4) 最后再得到 webpack-cli 后进行调用;