>[success] # webpack cli 源码分析
~~~
1.上一节可以知道,在启动过程中,webpack确保你已经安装了cli后,会开始启动cli,这时候后
就会到'node_modules\webpack-cli\bin\cli.js'
~~~
>[info] ## 分析
~~~
1.打开文件后发现是一个立即执行函数,
~~~
>[danger] ##### 判断启用本地还是全局cli
~~~js
1.下面这段主要是'本地如果安装了webpack-cli,就用本地安装版本,不用全局的'
2.可以打开'import-local' 这个库的源码来看一下'use strict';
const path = require('path');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');
module.exports = filename => {
// 获取文件的根目录
const globalDir = pkgDir.sync(path.dirname(filename));
// 获取文件的绝对路径
const relativePath = path.relative(globalDir, filename);
// 获取根目录下package.json文件信息
const pkg = require(path.join(globalDir, 'package.json'));
// 取出package.json 的name 一般name 都是文件名,根据相对路径
// 来判断改模块是否存在如果不存在返回undefined
const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
// Use `path.relative()` to detect local package installation,
// because __filename's case is inconsistent on Windows
// Can use `===` when targeting Node.js 8
// See https://github.com/nodejs/node/issues/6624
return localFile && path.relative(localFile, filename) !== '' ? require(localFile) : null;
};
3.来看一下,在我的项目中,当在'cli' 文件的'__filename'传入打印后的一些值
'globalDir' -- G:\testJs\webpackTs1\node_modules\webpack-cli
'relativePath' -- bin\cli.js
'localFile' -- G:\testJs\webpackTs1\node_modules\webpack-cli\bin\cli.js
"path.join(globalDir, 'package.json')" -- webpack-cli\bin\cli.js
~~~
~~~
const importLocal = require("import-local");
// Prefer the local installation of webpack-cli
// 本地如果安装了webpack-cli,就用本地安装版本,不用全局的
if (importLocal(__filename)) { // 本地的返回值是null
return;
}
// 使用v8缓存的代码,从而加快实例化时间, “代码缓存”是由V8解析和编译完成的工作。
require("v8-compile-cache");
~~~
>[danger] ##### 引入处理错误的工具模块
~~~
const ErrorHelpers = require("./utils/errorHelpers");
~~~
>[warning] ### 处理不需要经过编译的命令
~~~
1.通过判断输入的的指令来是否在'./utils/constants'模块定义的'NON_COMPILATION_ARGS' 常量数组里,
如果存在整个程序结束并且去执行'./utils/prompt-command',如果不存在代码接着往下走
~~~
>[danger] ##### 看懂这段源码前需要知道的知识
~~~
1.如何在控制台输入命令并且获取?利用'process.argv' 获取的是一个数组'string[]'
const a = process.argv
console.log(a)
我们用node 运行上面代码(因为我是将这段代码放到了一个test.js文件中 )因此我在控制台输入的指令为
'node test.js param1 param2'
'打印的结果':(下面数组第0项和第1项是自带,数组后面的项是输入的参数)
[ 'D:\\nodjs\\node.exe', // 属性返回启动 Node.js 进程的可执行文件的绝对路径名
'G:\\testJs\\js\\test.js', // 正被执行的 JavaScript 文件的路径
'param1',
'param2' ]
~~~
>[danger] ##### ./utils/constants文件中的内容
~~~
const NON_COMPILATION_ARGS = [
"init", //创建一份 webpack 配置文件
"migrate", //进行 webpack 版本迁移
"add", //往 webpack 配置文件中增加属
"remove", //往 webpack 配置文件中删除属
"serve", //运行 webpack-serve
"generate-loader", //生成 webpack loader 代码
"generate-plugin", //生成 webpack plugin 代码
"info'" //返回与本地环境相关的一些信息
];
~~~
>[danger] ##### cli 这段的源码
~~~
1.这部分引入了 一个指令集合的数组'NON_COMPILATION_ARGS',里面开始一段比较有意思的逻辑
1.1.如果你输入的参数是'serve' 在开头的 话会被从接受控制台输出参数的 'process.argv'数组里清除
,有点抽象举个例子,当你输出指令是(这里我是windows系统所以路径反斜杠是朝着面的 )
'.\node_modules\.bin\webpack serve info' 此时你的'process.argv' 里返回的值如下
[ 'D:\\nodjs\\node.exe', // 属性返回启动 Node.js 进程的可执行文件的绝对路径名
'G:\\testJs\\js\\test.js', // 正被执行的 JavaScript 文件的路径
'serve',
'info' ]
但是不行我要把你serve 指令干掉变成
[ 'D:\\nodjs\\node.exe', // 属性返回启动 Node.js 进程的可执行文件的绝对路径名
'G:\\testJs\\js\\test.js', // 正被执行的 JavaScript 文件的路径
'info' ]
1.2.再利用数组find 方法返回第一个输入指令在指令集合的指令
1.3.并且执行'/utils/prompt-command '模块的代码,并且终止执行接下来的代码
2.不往'/utils/prompt-command'代码里面深入看,来猜为什么在这里清除掉了,看一下需要调用方法参数
require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
2.1.可以发现他需要两个参数,一个是最先找到指令集合中的指令,一个是输入的指令,
那他的逻辑很有可能是先执行输入指令中第一个符合,指令集合的中指令,在执行后续指令
那么serve 很有可能和其他指令不同,导致循环执行serve,现在都是猜测
来一个数组find 的小案例
[1,2,3].find(item=> item ===3) // 3
~~~
~~~
const {
NON_COMPILATION_ARGS
} = require("./utils/constants");
// 查找输入指令是否在指令集合中
const NON_COMPILATION_CMD = process.argv.find(arg => {
if (arg === "serve") { // 输入的指令如果为serve
// 下面这两行比较有意思 在 process.argv 接受的指令中将serve清除掉
// 第一个先过滤,第二步把过滤的值重新赋值
global.process.argv = global.process.argv.filter(a => a !== "serve");
process.argv = global.process.argv;
}
// 数组的find 方法返回输入指令中,第一个符合指令集合中的值
return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) { //如果是集合中的指令 就去执行导入这个模块带并且结束下面的代码
return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
}
~~~
>[danger] ##### ./utils/prompt-command 里面做了什么
~~~
1.分析'require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);' 有两个参数
1.1.'NON_COMPILATION_CMD,' 输入指令中第一个在指令集合的参数
1.2.'...process.argv' 如果是输入指令中第一个参数'serve' 则不包含的,输入指令集合数组
3.整个这个文件代码也是分为四个部分,三个工具方法,一个执行方法
3.1.const runCommand = (command, args) => {...} // 执行某个命令,这里是本地安装
3.2.const npmGlobalRoot =() => {...} // 执行某个命令,这里是全局安装
3.3.const runWhenInstalled = (packages, pathForCmd, ...args) => {...} // 执行执行命令对应的方法
3.4.promptForInstallation(packages, ...args){...} // 来决定是执行命令还是安装执行命令的包
~~~
* 把3.1 - 3.3的 源码直接贴出来(这里就直接参考第一章对安装命令方法的讲解)
~~~
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();
}
});
});
};
const npmGlobalRoot = () => {
const cp = require("child_process");
return new Promise((resolve, reject) => {
const command = cp.spawn("npm", ["root", "-g"]);
command.on("error", error => reject(error));
command.stdout.on("data", data => resolve(data.toString()));
command.stderr.on("data", data => reject(data));
});
};
const runWhenInstalled = (packages, pathForCmd, ...args) => {
const currentPackage = require(pathForCmd);
const func = currentPackage.default;
if (typeof func !== "function") {
throw new Error(`@webpack-cli/${packages} failed to export a default function`);
}
return func(...args);
};
~~~
* promptForInstallation
~~~
module.exports = function promptForInstallation(packages, ...args) {
const nameOfPackage = "@webpack-cli/" + packages;// 拼接包名例如指令serve 拼接出@webpack-cli/serve
let packageIsInstalled = false; // 标记包是否安装的开关
let pathForCmd;
try {
const path = require("path");
const fs = require("fs");
// process.cwd() 方法会返回 Node.js 进程的当前工作目录
// pathForCmd 就会拼出一个目录例如指令是serve 当前工作目录/"node_modules/@webpack-cli/serve
pathForCmd = path.resolve(process.cwd(), "node_modules", "@webpack-cli", packages);
if (!fs.existsSync(pathForCmd)) { // 如果当前工作目录不存在这个包就去全局目录里找
const globalModules = require("global-modules");
pathForCmd = globalModules + "/@webpack-cli/" + packages;
require.resolve(pathForCmd);
} else { // 存在 就走着个
require.resolve(pathForCmd);
}
packageIsInstalled = true; // 并且加安装开关标志成true 表示这个包是安装过得
} catch (err) { // 两个地方都没找到进入catch
packageIsInstalled = false;
}
if (!packageIsInstalled) { // 两个地方都没找到开始安装包
const path = require("path");
const fs = require("fs");
const readLine = require("readline");
const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));
const packageManager = isYarn ? "yarn" : "npm";
const options = ["install", "-D", nameOfPackage];
if (isYarn) {
options[0] = "add";
}
if (packages === "init") {// init 包比较特别会被安装到全局目录里
if (isYarn) {
options.splice(1, 1); // remove '-D'
options.splice(0, 0, "global");
} else {
options[1] = "-g";
}
}
const commandToBeRun = `${packageManager} ${options.join(" ")}`;
const question = `Would you like to install ${packages}? (That will run ${commandToBeRun}) (yes/NO) : `;
console.error(`The command moved into a separate package: ${nameOfPackage}`);
const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stdout
});
questionInterface.question(question, answer => {
questionInterface.close();
switch (answer.toLowerCase()) {
case "y":
case "yes":
case "1": {
runCommand(packageManager, options)
.then(_ => {
if (packages === "init") {// init 包比较特别会被安装到全局目录里
npmGlobalRoot()
.then(root => {
const pathtoInit = path.resolve(root.trim(), "@webpack-cli", "init");
return pathtoInit;
})
.then(pathForInit => {
return require(pathForInit).default(...args);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
return;
}
pathForCmd = path.resolve(process.cwd(), "node_modules", "@webpack-cli", packages);
// 安装好后执行这个安装模块
return runWhenInstalled(packages, pathForCmd, ...args);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
break;
}
default: { // 不同意安装
console.error(`${nameOfPackage} needs to be installed in order to run the command.`);
process.exitCode = 1;
break;
}
}
});
} else {
return runWhenInstalled(packages, pathForCmd, ...args);// 执行指令对应的模块
}
};
~~~
>[warning] ### 处理需要经过编译的命令
~~~
1.'.\node_modules\.bin\webpack help' 当我们输入help 时候可以发现控制台会出现,额外的不仅仅只在
上面集合指令数组中才有的指令,这些指令的执行分析系
~~~
>[danger] ##### yargs
~~~
1.如何在控制台生成这些帮助指令实际使用'yargs' 库,在'./config/config-yargs'也配置这些指令,
打开这文件其实可以看到下面这些指令都是在不同的组里面,这九组的含义
1.1.'Config options': 配置相关参数(文件名称、运行环境等)
1.2.'Basic options': 基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)
1.3.'Module options': 模块参数,给 loader 设置扩展
1.4.'Output options': 输出参数(输出路径、输出文件名称)
1.5.'Advanced options': 高级用法(记录设置、缓存设置、监听频率、bail等)
1.6.'Resolving options': 解析参数(alias 和 解析的文件后缀设置)
1.7.'Optimizing options': 优化参数
1.8.'Stats options': 统计参数
1.9.'options': 通用参数(帮助命令、版本信息等)
~~~
* ./config/config-yargs 指令组
![](https://img.kancloud.cn/45/fb/45fb5fa0b3169d7c03bfdc237570b8d1_526x261.png)
![](https://img.kancloud.cn/ed/98/ed98115e573abf9337e7306366c1cd2f_595x426.png)
![](https://img.kancloud.cn/45/27/4527ba885bcdcc3ab96c5b7de6174db1_614x258.png)
~~~
// 声明一些基本的帮助信息
const yargs = require("yargs").usage(`webpack-cli ${require("../package.json").version}
Usage: webpack-cli [options]
webpack-cli [options] --entry <entry> --output <output>
webpack-cli [options] <entries...> --output <output>
webpack-cli <command> [options]
For more information, see https://webpack.js.org/api/cli/.`);
// 将这个yargs 对象加入config-yargs模块
require("./config/config-yargs")(yargs);
~~~
>[danger] ##### 指令执行
~~~
1.process.argv.slice(2) 获取输出的指令,要知道这个前两项里面不是我们输入的真正意义上的指令
2.回调函数中 argv err output 这三个参数参考文档
https://github.com/yargs/yargs/blob/HEAD/docs/api.md#parseargs-context-parsecallback
~~~
~~~
yargs.parse(process.argv.slice(2), (err, argv, output) => {...})
~~~
* 在yargs.parse 回调函数中接着会看到这部分的代码
~~~
1.根据命令行参数,获取并解析配置文件配置信息options,并结合命令行参数再次处理配置信息options,
校验配置项合法性。
2.捕获异常,webpack模块找不到,没安装的话提示下。
3.非校验错误,直接抛出错误。
4.校验错误等,简洁化处理保留必要错误信息。
5.结束。返回
~~~
* 对第一条详细解释一下
~~~
1.options = require("./utils/convert-argv")(argv);会根据你指令返回的配置项将这个转换成webpack格式
输入的指令'.\node_modules\.bin\webpack optimize-max-chunks'
~~~
* argv 这个对象
![](https://img.kancloud.cn/30/60/3060183c4bc5dc682df5ab2c4362fb85_505x157.png)
* 将argv 这个对象通过./utils/convert-argv解析后打印的opition值
![](https://img.kancloud.cn/5a/a3/5aa3c2ec74d10b96db203a3464d6c43f_485x299.png)
* 代码
![](https://img.kancloud.cn/1f/cc/1fcc6df4b3548f684e56817ed86dadd6_712x478.png)
>[danger] ##### ifArg方法
~~~
1.从命令行取参数值,并且执行传入的函数,函数参数时从命令行取的参数值。兼容数组,遍历执行
~~~
~~~
function ifArg(name, fn, init) {
if (Array.isArray(argv[name])) {
if (init) init();
argv[name].forEach(fn);
} else if (typeof argv[name] !== "undefined") {
if (init) init();
fn(argv[name], -1);
}
}
~~~
>[danger] ##### processOptions
~~~
1.这个函数主要会引入一个webpack,并且把这个配置项传给webpakc
注这里代码太多了可以自己打开慢慢看
~~~
>[danger] ##### cli 实际做了什么
~~~
1.webpack-cli对配置文件和命令行参数进行转换最终生成配置选项参数 options
最终会根据配置参数实例化 webpack 对象,然后执行构建流程
~~~
- 工程化 -- Node
- vscode -- 插件
- vscode -- 代码片段
- 前端学会调试
- 谷歌浏览器调试技巧
- 权限验证
- 包管理工具 -- npm
- 常见的 npm ci 指令
- npm -- npm install安装包
- npm -- package.json
- npm -- 查看包版本信息
- npm - package-lock.json
- npm -- node_modules 层级
- npm -- 依赖包规则
- npm -- install 安装流程
- npx
- npm -- 发布自己的包
- 包管理工具 -- pnpm
- 模拟数据 -- Mock
- 页面渲染
- 渲染分析
- core.js && babel
- core.js -- 到底是什么
- 编译器那些术语
- 词法解析 -- tokenize
- 语法解析 -- ast
- 遍历节点 -- traverser
- 转换阶段、生成阶段略
- babel
- babel -- 初步上手之了解
- babel -- 初步上手之各种配置(preset-env)
- babel -- 初步上手之各种配置@babel/helpers
- babel -- 初步上手之各种配置@babel/runtime
- babel -- 初步上手之各种配置@babel/plugin-transform-runtime
- babel -- 初步上手之各种配置(babel-polyfills )(未来)
- babel -- 初步上手之各种配置 polyfill-service
- babel -- 初步上手之各种配置(@babel/polyfill )(过去式)
- babel -- 总结
- 各种工具
- 前端 -- 工程化
- 了解 -- Yeoman
- 使用 -- Yeoman
- 了解 -- Plop
- node cli -- 开发自己的脚手架工具
- 自动化构建工具
- Gulp
- 模块化打包工具为什么出现
- 模块化打包工具(新) -- webpack
- 简单使用 -- webpack
- 了解配置 -- webpack.config.js
- webpack -- loader 浅解
- loader -- 配置css模块解析
- loader -- 图片和字体(4.x)
- loader -- 图片和字体(5.x)
- loader -- 图片优化loader
- loader -- 配置解析js/ts
- webpack -- plugins 浅解
- eslit
- plugins -- CleanWebpackPlugin(4.x)
- plugins -- CleanWebpackPlugin(5.x)
- plugin -- HtmlWebpackPlugin
- plugin -- DefinePlugin 注入全局成员
- webapck -- 模块解析配置
- webpack -- 文件指纹了解
- webpack -- 开发环境运行构建
- webpack -- 项目环境划分
- 模块化打包工具 -- webpack
- webpack -- 打包文件是个啥
- webpack -- 基础配置项用法
- webpack4.x系列学习
- webpack -- 常见loader加载器
- webpack -- 移动端px转rem处理
- 开发一个自己loader
- webpack -- plugin插件
- webpack -- 文件指纹
- webpack -- 压缩css和html构建
- webpack -- 清里构建包
- webpack -- 复制静态文件
- webpack -- 自定义插件
- wepack -- 关于静态资源内联
- webpack -- source map 对照包
- webpack -- 环境划分构建
- webpack -- 项目构建控制台输出
- webpack -- 项目分析
- webpack -- 编译提速优护体积
- 提速 -- 编译阶段
- webpack -- 项目优化
- webpack -- DefinePlugin 注入全局成员
- webpack -- 代码分割
- webpack -- 页面资源提取
- webpack -- import按需引入
- webpack -- 摇树
- webpack -- 多页面打包
- webpack -- eslint
- webpack -- srr打包后续看
- webpack -- 构建一个自己的配置后续看
- webpack -- 打包组件和基础库
- webpack -- 源码
- webpack -- 启动都做了什么
- webpack -- cli做了什么
- webpack - 5
- 模块化打包工具 -- Rollup
- 工程化搭建代码规范
- 规范化标准--Eslint
- eslint -- 扩展配置
- eslint -- 指令
- eslint -- vscode
- eslint -- 原理
- Prettier -- 格式化代码工具
- EditorConfig -- 编辑器编码风格
- 检查提交代码是否符合检查配置
- 整体流程总结
- 微前端
- single-spa
- 简单上手 -- single-spa
- 快速理解systemjs
- single-sap 不使用systemjs
- monorepo -- 工程
- Vue -- 响应式了解
- Vue2.x -- 源码分析
- 发布订阅和观察者模式
- 简单 -- 了解响应式模型(一)
- 简单 -- 了解响应式模型(二)
- 简单 --了解虚拟DOM(一)
- 简单 --了解虚拟DOM(二)
- 简单 --了解diff算法
- 简单 --了解nextick
- Snabbdom -- 理解虚拟dom和diff算法
- Snabbdom -- h函数
- Snabbdom - Vnode 函数
- Snabbdom -- init 函数
- Snabbdom -- patch 函数
- 手写 -- 虚拟dom渲染
- Vue -- minVue
- vue3.x -- 源码分析
- 分析 -- reactivity
- 好文
- grpc -- 浏览器使用gRPC
- grcp-web -- 案例
- 待续