人人都看得懂的 webpack 源码(2)webpack-cli-启动 -1

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 安装

  1. 执行安装命令
shell 复制代码
npm install -D webpack webpack-cli  
  1. 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 用法

  1. 用法
shell 复制代码
npx webpack watch [options]  
  1. 示例
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 会引导用户进行安装,具体工作如下:

  1. 包管理器检查
    根据 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";  
}  
  1. 创建 REPL 接口
    webpack 通过 Node.js 提供的 readline 模块完成交互式命令行界面,传送门readline
js 复制代码
const questionInterface = readLine.createInterface({  
input: process.stdin,  
output: process.stderr  
});  
  1. REPL 询问用户并处理用户输入
    下面会询问用户是否打算安装 webpack
js 复制代码
const question = `Do you want to install 'webpack-cli' (yes/no): `;  

发起询问通过 questionInterface.question 方法:

js 复制代码
// 询问  
questionInterface.question(question, answer => {  
// 这个回调用户接受用户输入  
});  
  1. 处理用户输入
    如果用户输入不是 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 相关作用及部分实现,主要包含以下内容:

  1. webpack-cli 的安装及用法;
  2. 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 后进行调用;
相关推荐
阿珊和她的猫1 小时前
v-scale-scree: 根据屏幕尺寸缩放内容
开发语言·前端·javascript
PAK向日葵3 小时前
【算法导论】PDD 0817笔试题题解
算法·面试
加班是不可能的,除非双倍日工资5 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi6 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip6 小时前
vite和webpack打包结构控制
前端·javascript
excel7 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国7 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼7 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy7 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT7 小时前
promise & async await总结
前端