从输入指令开始
平时使用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-require
const pkg = require(pkgPath);
// eslint-disable-next-line node/no-missing-require
require(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 object
const 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 commands
const 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 commands
const 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 exit
this.program.exitOverride(async (error) => {});
// Default `--color` and `--no-color` options
const 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...]` command
const 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 action
this.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();
}
}
}
然后就进入了webpack-cli里面的createCompiler方法,再调用this.webpack创建compiler对象,this.webpack在上面的run方法里已经赋值好,就是将webpack包加载了进来
在webpack这个包中,很容易找到,其调用的方法是:
const webpack = (options, callback) => {
// create方法定义的部分
if (callback) {
try {
const { compiler, watch, watchOptions } = create();
if (watch) {
compiler.watch(watchOptions, callback);
} else {
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});
}
return compiler;
} catch (err) {
process.nextTick(() => callback(err));
return null;
}
}
}
注意到webpack方法中通过调用create方法返回了compiler watch等对象,其实create的逻辑较为简单,仅仅是判断传入的参数是数组还是单个对象,从而决定调用createMultiCompiler或者createCompiler,而createMultiCompiler最终也会调用createCompiler,因此我们接下来重点关注createCompiler方法:
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context, options);
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
}
这是一个很重要的方法,因为我们平时在使用webpack时,插件是我们最关心的话题之一,在这个方法中,就会调用插件的apply方法,进而让插件执行compiler上从Tapable继承来的hook属性的订阅操作
当然,除了插件相关的行为外,还有其他一些事情,大概分为以下几部分:
- 处理并规整options(getNormalizedWebpackOptions applyWebpackOptionsBaseDefaults)
- 创建compiler对象
- 开发者配置在options中的插件的apply方法的执行(里面主要就是往各种对象上订阅方法),执行到此处时肯定是先给compiler上从Tapable继承来的hook属性上订阅了若干方法
- 根据options收集webpack内置的各种插件,然后执行插件的apply方法,继续给compiler上从Tapable继承来的hook属性上订阅若干方法(new WebpackOptionsApply().process(options, compiler))
在所有这些插件中,有一个需要额外注意一下,因为这个插件是下一步make阶段(也就是从entry模块开始解析,然后遍历所有依赖再生成依赖树):
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
EntryOptionPlugin的apply方法如下:
class EntryOptionPlugin {
/**
* @param {Compiler} compiler the compiler instance one is tapping into
* @returns {void}
*/
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
可以看到,它在hooks.entryOption上订阅了一个回调,而且在WebpackOptionsApply中订阅代码的下方立刻进行了发布(compiler.hooks.entryOption.call(options.context, options.entry)),即执行了
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
EntryOptionPlugin.applyEntryOption是EntryOptionPlugin插件的一个静态方法,我们来看下它有干了什么事情:
static applyEntryOption(compiler, context, entry) {
if (typeof entry === "function") {
const DynamicEntryPlugin = require("./DynamicEntryPlugin");
new DynamicEntryPlugin(context, entry).apply(compiler);
} else {
const EntryPlugin = require("./EntryPlugin");
for (const name of Object.keys(entry)) {
const desc = entry[name];
const options = EntryOptionPlugin.entryDescriptionToOptions(
compiler,
name,
desc
);
for (const entry of desc.import) {
new EntryPlugin(context, entry, options).apply(compiler);
}
}
}
}
通常来讲,我们传进来的option.entry都是对象类型的,所以大多数情况下会走到else分支中,在这个分支里,可以看到又加载了一个名为EntryPlugin的插件,EntryPlugin的每个实例都有一个对应的entry和其映射,我们打开EntryPlugin的源码:
class EntryPlugin {
/**
* An entry plugin which will handle
* creation of the EntryDependency
*
* @param {string} context context path
* @param {string} entry entry path
* @param {EntryOptions | string=} options entry options (passing a string is deprecated)
*/
constructor(context, entry, options) {
this.context = context;
this.entry = entry;
this.options = options || "";
}
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
}
可以看到,在apply里面,给hooks.make订阅了一个回调,里面这行代码很关键:
compilation.addEntry(context, dep, options, err => {
callback(err);
});
因为它就是从初始化进入make阶段的桥梁,从这里开始,webpack将从entry入口文件开始递归分析,其在遍历过程中拿到的每个文件都会经历如下步骤:
- 引用路径resolve
- 过滤匹配的loader并对文件进行处理
- 生成ast树,并从ast树中拿到文件的依赖
- 对每个文件重复执行上述流程,递归遍历直到叶子结点
在上面的过程中,会在不同阶段分别触发不同的对象之前订阅过的方法
到此,初始化流程基本完毕,接下来进入make阶段