>[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 对象,然后执行构建流程 ~~~