文章首发于我的博客 https://github.com/mcuking/bl…
本文以 webpack 源码来剖析其外部的工作流程,筹备剖析的版本为 4.41.5。
首先咱们要确认的是 webpack 的执行入口文件,通过查看 node_modules 中 webpack 的 package.json 的 bin 字段(如下),咱们能够晓得入口文件是 bin 文件下的 webpack.js,即 node_modules\webpack\bin\webpack.js
。
"bin": {"webpack": "./bin/webpack.js"},
剖析 webpack 入口文件:webpack.js
文件代码并不多,总共有 171 行,次要分为 6 个局部:
- 失常执行返回
process.exitCode = 0;
- 定义了一个运行某个命令的办法 runCommand
const runCommand = (command, args) => {...};
- 定义了一个判断某个包是否装置的办法 isInstalled
const isInstalled = packageName => {
try {require.resolve(packageName);
return true;
} catch (err) {return false;}
};
- 定义了两个 webpack 可用的 CLI:webpack-cli 和 webpack-command。其中 installed 属性就是调用了下面的 isInstalled 办法来计算的。
const CLIs = [
{
name: "webpack-cli",
package: "webpack-cli",
binName: "webpack-cli",
alias: "cli",
installed: isInstalled("webpack-cli"),
recommended: true,
url: "https://github.com/webpack/webpack-cli",
description: "The original webpack full-featured CLI."
},
{
name: "webpack-command",
package: "webpack-command",
binName: "webpack-command",
alias: "command",
installed: isInstalled("webpack-command"),
recommended: false,
url: "https://github.com/webpack-contrib/webpack-command",
description: "A lightweight, opinionated webpack CLI."
}
];
- 紧接着计算出曾经装置的 CLI
const installedClis = CLIs.filter(cli => cli.installed);
- 而后依据装置 CLI 的数量进行解决
if (installedClis.length === 0) {...}
else if(installedClis.length === 1) {...}
else {...}
如果一个都没有装置,则会提醒是否要装置 webpack-cli,如果批准则主动帮你装置;如果装置了其中一个,会间接应用那个;如果装置了俩个,会提醒你删掉其中一个 CLI。
通过下面的剖析,咱们能够确认 webpack 最终会找到 webpack-cli 或 webpack-command 这个 npm 包,并执行这个 CLI。
那么咱们就去看下 webpack-cli 具体做了什么工作。
剖析 webpack-cli 运行机制
以后剖析的 webpack-cli 版本为 3.3.10,通过 webpack-cli 包的 package.json 的 bin 字段咱们能够确认 webpack-cli 的入口执行文件是 node_modules\webpack-cli\bin\cli.js
"bin": {"webpack-cli": "./bin/cli.js"},
而后咱们持续剖析 cli.js 做了些什么,通过大抵查看能够确认的是 webpack-cli 的业务逻辑并不简单,次要文件就是 cli.js,其余都是 utils 或 config 文件,而 cli.js 的代码也只有 366 行而已。上面咱们就具体分析下 cli.js 里到底做了些什么。
首先结尾处对用户输出的参数进行了分类,判断参数中有局部在 NON_COMPILATION_ARGS 中,则调用 utils 中 prompt-command 文件默认导出的办法,并将该命令和输出的参数传入到该办法。
const NON_COMPILATION_CMD = process.argv.find(arg => {if (arg === "serve") {global.process.argv = global.process.argv.filter(a => a !== "serve");
process.argv = global.process.argv;
}
return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) {return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
}
对此能够了解为,webpack-cli 局部命令是不须要进行编译的,即初始化一个 compiler 对象。那么咱们来看下不须要编译的命令都有哪些,以及它们的作用。
const NON_COMPILATION_ARGS = [
"init", // 创立一份 webpack 配置文件
"migrate", // 进行 webpack 版本迁徙
"serve", // 运行 webpack-serve
"generate-loader", // 生成 webpack loader 代码
"generate-plugin", // 生成 webpack plugin 代码
"info" // 返回与本地环境相干的一些信息
];
而后咱们再看下 ./utils/prompt-command
下的默认导出办法到底做了什么事,相干代码如下:
module.exports = function promptForInstallation(packages, ...args) {
try {const path = require("path");
const fs = require("fs");
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;
} catch (err) {packageIsInstalled = false;}
if (!packageIsInstalled) {...}
}
promptForInstallation 办法接管到命令后会判断这个命令对应的包是否装置过,例如在 @webpack-cli 文件夹下是否存在 init 包,如果不存在,则提醒装置,装置后则运行。如果存在则间接运行。
接下来咱们回到主流程,看下 cli.js 前面的逻辑。
const yargs = require("yargs").usage(`webpack-cli ${require("../package.json").version}
require("./config/config-yargs")(yargs);
yargs.parse(process.argv.slice(2), (err, argv, output) => {...})
./config/config-yargs 文件的默认导出的办法
module.exports = function(yargs) {
yargs
.help("help")
.alias("help", "h")
.version()
.alias("version", "v")
.options({
config: {
type: "string",
describe: "Path to the config file",
group: CONFIG_GROUP,
defaultDescription: "webpack.config.js or webpackfile.js",
requiresArg: true
},
...
);
}
能够看到 webpack-cli 用到了 yargs 工具来构建交互式命令行工具,它能够提供命令和分组参数,并可能动静生成 help 帮忙信息。而 config-yargs 文件中的办法的作用就是就是对 yargs 进行配置。
接下来咱们持续返回 cli.js 看下 yargs.parse 办法内做了什么。
yargs.parse(process.argv.slice(2), (err, argv, output) => {
...
try {options = require("./utils/convert-argv")(argv);
} catch(e) {...}
...
})
咱们能够看到接下来是调用了 ./utils/convert-argv
文件下的默认办法,并传入了输出参数。那么看看 ./utils/convert-argv
中的这个办法到底做了什么呢?
module.exports = function(...args) {if (argv.config) {} else {const defaultConfigFileNames = ["webpack.config", "webpackfile"].join("|");
const webpackConfigFileRegExp = `(${defaultConfigFileNames})(${extensions.join("|")})`;
const pathToWebpackConfig = findup(webpackConfigFileRegExp);
...
}
}
因为代码较多,我这里就只摘抄了局部要害代码,咱们不难发现这个办法次要作用就是生成 webpack 的配置项,例如 output 等,而起源次要有命令行的输出和 webpack.config.js 等文件。
再回到 cli.js,当生成完配置项 options,接下来又做了什么呢?
yargs.parse(process.argv.slice(2), (err, argv, output) => {
...
function processOptions(options) {
let compiler;
try {compiler = webpack(options);
} catch (err) {...}
if (firstOptions.watch || options.watch) {
const watchOptions =
firstOptions.watchOptions || options.watchOptions || firstOptions.watch || options.watch || {};
...
compiler.watch(watchOptions, compilerCallback);
...
} else {compiler.run((err, stats) => {if (compiler.close) {
compiler.close(err2 => {compilerCallback(err || err2, stats);
});
} else {compilerCallback(err, stats);
}
});
}
}
processOptions(options)
})
咱们能够看到,最初阶段调用了 processOptions 办法,外面则是获取到了 webpack 的一个 compiler 实例对象(后续咱们会介绍 webpack 的 Compiler),判断以后是否是 watch 形式,是的话就调用 compiler 实例上的 watch 办法来运行,否则调用 run 办法来运行,也就是开始了真正的打包构建流程。
总结下,webpack-cli 次要的作用就是:
- 引入 yargs,提供一个交互式命令行工具
- 对配置文件和命令行参数进行转换,最终生成配置选项参数;
- 而后依据配置实例化 webpack 对象,而后执行构建流程。
在下一篇咱们持续剖析 webpack 中的构建机制。