人人都看得懂的 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 后进行调用;
相关推荐
Fan_web10 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常11 分钟前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho2 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常3 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记4 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java4 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele4 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范