从输入指令开始
平时使用webpack时,大致可以分为如下几种形式:
1、直接在控制台输入webpack指令,这种情况下最直接执行到的文件是./node_modules/bin/webpack.js或者/node_modules/webpack/bin/webpack.js,前者为后者的软链
2、通常写好一个类似dev-server.js/build.js的node脚本,通过npm run dev/npn run build指令去执行,这种情况通常会require(‘webpack’)直接拿到位于./node_modules/webpack/lib/webpack.js的webpack方法传入config,手动调用compiler.run执行
3、第三方库封装好的一些指令,例如create-react-app vue-cli-service,这种情况和第2种类似
接下来我们重点分析第1种情况是怎么调用到./node_modules/webpack/lib/webpack.js的:
在/node_modules/webpack/bin/webpack.js文件中,会调用runCli:
const cli = {name: "webpack-cli",package: "webpack-cli",binName: "webpack-cli",installed: isInstalled("webpack-cli"),url: "https://github.com/webpack/webpack-cli"};if (!cli.installed) {// 没有安装cli的情况,可以忽略} else {runCli(cli);}
const runCli = cli => {const path = require("path");const pkgPath = require.resolve(`${cli.package}/package.json`);// eslint-disable-next-line node/no-missing-requireconst pkg = require(pkgPath);// eslint-disable-next-line node/no-missing-requirerequire(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));};
很显然,上面代码中第7行,require的路径为
./node_modules/webpack-cli/bin/cli.js
调用require解析完该文件之后,就会执行里面的runCLI方法:
#!/usr/bin/env node"use strict";const importLocal = require("import-local");const runCLI = require("../lib/bootstrap");if (!process.env.WEBPACK_CLI_SKIP_IMPORT_LOCAL) {// Prefer the local installation of `webpack-cli`if (importLocal(__filename)) {return;}}process.title = "webpack";runCLI(process.argv);
可以进一步去../lib/bootstrap寻找runCLI的定义:
const WebpackCLI = require("./webpack-cli");const runCLI = async (args) => {// Create a new instance of the CLI objectconst cli = new WebpackCLI();try {await cli.run(args);} catch (error) {cli.logger.error(error);process.exit(2);}};module.exports = runCLI;
再进一步去./webpack-cli中寻找WebpackCLI.run方法的定义,这个方法非常长,大约有750行,以下是简化后的结果:
async run(args, parseOptions) {// Built-in internal commandsconst buildCommandOptions = {name: "build [entries...]",alias: ["bundle", "b"],description: "Run webpack (default command, can be omitted).",usage: "[entries...] [options]",dependencies: [WEBPACK_PACKAGE],};const watchCommandOptions = {name: "watch [entries...]",alias: "w",description: "Run webpack and watch for files changes.",usage: "[entries...] [options]",dependencies: [WEBPACK_PACKAGE],};const versionCommandOptions = {name: "version [commands...]",alias: "v",description:"Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",};const helpCommandOptions = {name: "help [command] [option]",alias: "h",description: "Display help for commands and options.",};// Built-in external commandsconst externalBuiltInCommandsInfo = [{name: "serve [entries...]",alias: ["server", "s"],pkg: "@webpack-cli/serve",},{name: "info",alias: "i",pkg: "@webpack-cli/info",},{name: "init",alias: ["create", "new", "c", "n"],pkg: "@webpack-cli/generators",},{name: "loader",alias: "l",pkg: "@webpack-cli/generators",},{name: "plugin",alias: "p",pkg: "@webpack-cli/generators",},{name: "migrate",alias: "m",pkg: "@webpack-cli/migrate",},{name: "configtest [config-path]",alias: "t",pkg: "@webpack-cli/configtest",},];const knownCommands = [buildCommandOptions,watchCommandOptions,versionCommandOptions,helpCommandOptions,...externalBuiltInCommandsInfo,];const getCommandName = (name) => name.split(" ")[0];const isKnownCommand = (name) =>knownCommands.find((command) =>getCommandName(command.name) === name ||(Array.isArray(command.alias) ? command.alias.includes(name) : command.alias === name),);const isCommand = (input, commandOptions) => {const longName = getCommandName(commandOptions.name);if (input === longName) {return true;}if (commandOptions.alias) {if (Array.isArray(commandOptions.alias)) {return commandOptions.alias.includes(input);} else {return commandOptions.alias === input;}}return false;};const findCommandByName = (name) =>this.program.commands.find((command) => name === command.name() || command.aliases().includes(name),);const isOption = (value) => value.startsWith("-");const isGlobalOption = (value) =>value === "--color" ||value === "--no-color" ||value === "-v" ||value === "--version" ||value === "-h" ||value === "--help";const loadCommandByName = async (commandName, allowToInstall = false) => {};// Register own exitthis.program.exitOverride(async (error) => {});// Default `--color` and `--no-color` optionsconst cli = this;this.program.option("--color", "Enable colors on console.");this.program.on("option:color", function () {const { color } = this.opts();cli.isColorSupportChanged = color;cli.colors = cli.createColors(color);});this.program.option("--no-color", "Disable colors on console.");this.program.on("option:no-color", function () {const { color } = this.opts();cli.isColorSupportChanged = color;cli.colors = cli.createColors(color);});// Make `-v, --version` options// Make `version|v [commands...]` commandconst outputVersion = async (options) => {};this.program.option("-v, --version","Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",);const outputHelp = async (options, isVerbose, isHelpCommandSyntax, program) => {}this.program.helpOption(false);this.program.addHelpCommand(false);this.program.option("-h, --help [verbose]", "Display help for commands and options.");let isInternalActionCalled = false;// Default actionthis.program.usage("[options]");this.program.allowUnknownOption(true);this.program.action(async (options, program) => {});await this.program.parseAsync(args, parseOptions);}
可以看到最后对我们输入的参数进行了解析,虽然我们并不能确定parseAsync这个方法干了什么,但我们可以猜测到,如果我们想要执行开发或生产环境的编译,一定要走到一个加载webpack并传入配置执行的方法里面,这个方法就是runWebpack
async runWebpack(options, isWatchCommand) {
// eslint-disable-next-line prefer-const
let compiler;
let createJsonStringifyStream;
if (options.json) {
const jsonExt = await this.tryRequireThenImport("@discoveryjs/json-ext");
createJsonStringifyStream = jsonExt.stringifyStream;
}
const callback = (error, stats) => {
if (error) {
this.logger.error(error);
process.exit(2);
}
if (stats.hasErrors()) {
process.exitCode = 1;
}
if (!compiler) {
return;
}
const statsOptions = compiler.compilers
? {
children: compiler.compilers.map((compiler) =>
compiler.options ? compiler.options.stats : undefined,
),
}
: compiler.options
? compiler.options.stats
: undefined;
// TODO webpack@4 doesn't support `{ children: [{ colors: true }, { colors: true }] }` for stats
const statsForWebpack4 = this.webpack.Stats && this.webpack.Stats.presetToOptions;
if (compiler.compilers && statsForWebpack4) {
statsOptions.colors = statsOptions.children.some((child) => child.colors);
}
if (options.json && createJsonStringifyStream) {
const handleWriteError = (error) => {
this.logger.error(error);
process.exit(2);
};
if (options.json === true) {
createJsonStringifyStream(stats.toJson(statsOptions))
.on("error", handleWriteError)
.pipe(process.stdout)
.on("error", handleWriteError)
.on("close", () => process.stdout.write("\n"));
} else {
createJsonStringifyStream(stats.toJson(statsOptions))
.on("error", handleWriteError)
.pipe(fs.createWriteStream(options.json))
.on("error", handleWriteError)
// Use stderr to logging
.on("close", () => {
process.stderr.write(
`[webpack-cli] ${this.colors.green(
`stats are successfully stored as json to ${options.json}`,
)}\n`,
);
});
}
} else {
const printedStats = stats.toString(statsOptions);
// Avoid extra empty line when `stats: 'none'`
if (printedStats) {
this.logger.raw(printedStats);
}
}
};
const env =
isWatchCommand || options.watch
? { WEBPACK_WATCH: true, ...options.env }
: { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, ...options.env };
options.argv = { ...options, env };
if (isWatchCommand) {
options.watch = true;
}
compiler = await this.createCompiler(options, callback);
if (!compiler) {
return;
}
const isWatch = (compiler) =>
compiler.compilers
? compiler.compilers.some((compiler) => compiler.options.watch)
: compiler.options.watch;
if (isWatch(compiler) && this.needWatchStdin(compiler)) {
process.stdin.on("end", () => {
process.exit(0);
});
process.stdin.resume();
}
}
}
