# 运行webpack发生了什么?
# 查找webpack入口文件
通过npm scripts运行webpack时候,npm其实是通过让命令行工具进入node_modules/.bin目录
查找是否存在webpack.cmd或者webpack.sh文件,如果存在就执行,不存在则抛出错误。
node_modules/bin目录下有这个命令,则如果局部安装依赖,则需要在依赖配置package.json中指定bin字段,
所以实际查找的入口文件就是 node_modules/webpack/bin/webpack.js。
# 分析webpack入口文件
/node_modules/webpack/bin/webpack.js
#!/usr/bin/env node
// @ts-ignore
process.exitCode = 0;
/**
* @param {string} command process to run
* @param {string[]} args commandline arguments
* @returns {Promise<void>} promise
*/
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();
}
});
});
};
/**
* @param {string} packageName name of the package
* @returns {boolean} is the package installed?
*/
const isInstalled = packageName => {
try {
require.resolve(packageName);
return true;
} catch (err) {
return false;
}
};
/**
* @typedef {Object} CliOption
* @property {string} name display name
* @property {string} package npm package name
* @property {string} binName name of the executable file
* @property {string} alias shortcut for choice
* @property {boolean} installed currently installed?
* @property {boolean} recommended is recommended
* @property {string} url homepage
* @property {string} description description
*/
/** @type {CliOption[]} */
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."
}
];
const installedClis = CLIs.filter(cli => cli.installed);
if (installedClis.length === 0) {
const path = require("path");
const fs = require("fs");
const readLine = require("readline");
let notify =
"One CLI for webpack must be installed. These are recommended choices, delivered as separate packages:";
for (const item of CLIs) {
if (item.recommended) {
notify += `\n - ${item.name} (${item.url})\n ${item.description}`;
}
}
console.error(notify);
const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));
const packageManager = isYarn ? "yarn" : "npm";
const installOptions = [isYarn ? "add" : "install", "-D"];
console.error(
`We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join(
" "
)}".`
);
const question = `Do you want to install 'webpack-cli' (yes/no): `;
const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr
});
questionInterface.question(question, answer => {
questionInterface.close();
const normalizedAnswer = answer.toLowerCase().startsWith("y");
if (!normalizedAnswer) {
console.error(
"You need to install 'webpack-cli' to use webpack via CLI.\n" +
"You can also install the CLI manually."
);
process.exitCode = 1;
return;
}
const packageName = "webpack-cli";
console.log(
`Installing '${packageName}' (running '${packageManager} ${installOptions.join(
" "
)} ${packageName}')...`
);
runCommand(packageManager, installOptions.concat(packageName))
.then(() => {
require(packageName);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
});
} else if (installedClis.length === 1) {
const path = require("path");
const pkgPath = require.resolve(`${installedClis[0].package}/package.json`);
const pkg = require(pkgPath);
require(path.resolve(
path.dirname(pkgPath),
pkg.bin[installedClis[0].binName]
));
} else {
console.warn(
`You have installed ${installedClis
.map(item => item.name)
.join(
" and "
)} together. To work with the "webpack" command you need only one CLI package, please remove one of them or use them directly via their binary.`
);
process.exitCode = 1;
}
分析webpack.js,可以了解到主要有几大部分:
- process.exitCode = 0
正常执行返回,如果exitCode!=0,则为抛出对应错误; - const runCommand = (command, args) => {...}
运行某个命令,并以promise形式返回; - const isInstall = packageName => {...}
判断某个包是否安装 - const CLIs = [...]
webpack可用的脚手架:webpack-cli和webpack-command - const installedClis = CLIs.filter(cli => cli.installed)
判断脚手架是否安装 - if (installedClis.length === 0) {...} else if (installedClis.length === 1) {...} else {...}
根据安装脚手架数量进行判断,如果没有安装则提示报错并提示安装流程;如果安装了一个运行安装的脚手架CLI; 如果安装了两个就抛出错误
所以,npm scripts运行命令行,最终会通过webpack查找webpack-cli/webpack-command,并且执行CLI。
# webpack-cli工作流程
分析不需要编译的命令,例如init、info等命令并不会实例化webpack对象, webpack不需要经过构建编译的过程。
/node_modules/webpack-cli/bin/cli.js
/** ./utils/constants ,webpack-cli提供的不需要编译的命令
* NON_COMPILATION_ARGS =
* ["init", 创建一份webpack配置文件
* "migrate", 进行webpack版本迁移
* "serve", 运行webpack-serve
* "generate-loader", 生成webpack loader代码
* "generate-plugin", 生成webpack plugin代码
* "info" 返回与本地环境相关的信息
];**/
const { NON_COMPILATION_ARGS } = require("./utils/constants");
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) {
/**
* // ./utils/prompt-command
* module.exports = function promptForInstallation(packages, ...args) {
* // 查找node_modules下是否有@webpack-cli下的包名
* const nameOfPackage = "@webpack-cli/" + packages;
* let packageIsInstalled = false;
* let pathForCmd;
* // 如果存在
* try {
* ...
* // 如果不存在当前项目下,则通过全局的依赖包查找返回完整路径
* if (!fs.existsSync(pathForCmd)) {...} else {
* // 否则在当前依赖包查找返回完整路径
* ...
* }
* packageIsInstalled = true;
* } catch (err) {
* packageIsInstalled = false;
* }
* // 如果不存在则提示并提供选择安装
* if (!packageIsInstalled) {...} else {
* return runWhenInstalled(packages, pathForCmd, ...args);
* }
* };
*
* */
return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
}
引入yargs,对命令行进行定制
设置参数分组,将命令划分为九类:
- Config options:配置相关参数(文件名称、运行环境等)
- Basic options:基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)
- Module options:模块参数,给loader设置扩展
- Output options:输出参数(输出路径、输出文件名称)
- Advanced options:高级用法(记录设置、缓存设置、监听频率、bail等)
- Resolving options:解析参数(alias和解析的文件后缀设置)
- Optimizing options:优化参数
- Stats options:统计参数
- options:通用参数(帮助命令、版本信息等)
// /node_modules/webpack-cli/bin/cli.js
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/.`);
/**
* const {
* CONFIG_GROUP, Config options:配置相关参数(文件名称、运行环境等)
* BASIC_GROUP, Basic options:基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)
* MODULE_GROUP, Module options:模块参数,给loader设置扩展
* OUTPUT_GROUP, Output options:输出参数(输出路径、输出文件名称)
* ADVANCED_GROUP, Advanced options:高级用法(记录设置、缓存设置、监听频率、bail等)
* RESOLVE_GROUP, Resolving options:解析参数(alias和解析的文件后缀设置)
* OPTIMIZE_GROUP, Optimizing options:优化参数
* DISPLAY_GROUP Stats options:统计参数
* } = GROUPS;
* ./config/config-yargs ,定义了一些基本信息help、version和options输入的一些命令
* 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
* },
* ...
* });
* };
*
*
* */
require("./config/config-yargs")(yargs);
分析命令行参数,对各个参数进行转换,组成编译配置项
引用webpack,根据配置项进行编译和构建
// /node_modules/webpack-cli/bin/cli.js
// 对命令行参数进行解析
yargs.parse(process.argv.slice(2), (err, argv, output) => {
...
// 定义options输入参数,传递给webpack,webpack-cli会从两处修改options
// 第一处是webpack.config.js配置文件
// 第二处是命令行中设置的参数
let options;
try {
// 组装options,转换为webpack可识别的参数
options = require("./utils/convert-argv")(argv);
} catch (err) {
// 如果报错则抛出错误
...
return;
}
/**
* When --silent flag is present, an object with a no-op write method is
* used in place of process.stout
*/
...
// 判断配置参数在对象上是否存在
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);
}
}
// 将组装的options传递进去
function processOptions(options) {
...
// 定义输出对象,webpack在输出时候就会读取outputOptions进而做相应的处理
let outputOptions = options.stats;
...
ifArg("display", function(preset) {
outputOptions = statsPresetToOptions(preset);
});
...
// 引入webpack
const webpack = require("webpack");
let lastHash = null;
let compiler;
try {
// 将组装完成的options传递给webpack,返回compiler对象
compiler = webpack(options);
} catch (err) {
//错误抛出
...
throw err;
}
...
if (outputOptions.infoVerbosity === "verbose") {
if (argv.w) {
compiler.hooks.watchRun.tap("WebpackInfo", compilation => {
const compilationName = compilation.name ? compilation.name : "";
console.error("\nCompilation " + compilationName + " starting…\n");
});
} else {
compiler.hooks.beforeRun.tap("WebpackInfo", compilation => {
const compilationName = compilation.name ? compilation.name : "";
console.error("\nCompilation " + compilationName + " starting…\n");
});
}
compiler.hooks.done.tap("WebpackInfo", compilation => {
const compilationName = compilation.name ? compilation.name : "";
console.error("\nCompilation " + compilationName + " finished\n");
});
}
//完成后,compiler回调
function compilerCallback(err, stats) {
//日志打印/错误抛出
...
}
// 如果设置watch,则以watch方式运行compiler.watch
if (firstOptions.watch || options.watch) {
const watchOptions =
firstOptions.watchOptions || options.watchOptions || firstOptions.watch || options.watch || {};
if (watchOptions.stdin) {
process.stdin.on("end", function(_) {
process.exit(); // eslint-disable-line
});
process.stdin.resume();
}
compiler.watch(watchOptions, compilerCallback);
if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
} else {
// 否则以compiler.run运行构建
compiler.run((err, stats) => {
if (compiler.close) {
compiler.close(err2 => {
compilerCallback(err || err2, stats);
});
} else {
compilerCallback(err, stats);
}
});
}
}
processOptions(options);
});
所以,webpack-cli执行的结果就是对配置文件和命令行参数进行转换,最终生成配置选项参数options。 然后根据配置参数实例化webpack对象,执行构建流程。
# 推荐阅读
npm scripts使用指南:http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html