调试工具:Chrome devtools
package.json的scripts添加配置 "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js --config ./config/webpack.prod.js"
在终端输入npm run debug之后,进入调试模式,此时断点在webpck文件的第一行。
首先会判断webpack-cli是否已安装,未安装的话,则安装,已安装,直接进入runCli
具体代码如下:
runCli的代码如下:
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
// 加载webpack-cli的bin文件,即webpack-cli/bin/cli.js
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};
进入webpack-cli的bin文件,看看会执行什么 内容很简单:
#!/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);
进入bootstrap文件,文件内容如下:
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;
重点是const cli = new WebpackCLI();和cli.run(args);(文件路径:lib/webpack-cli.js)
WebpackCLI构造函数定义了如下方法:
- colors--为控制台输出添加颜色
- logger--根据不同类型的输出信息,调用不同console方法
- program--解析用户输入命令。
接下来直接看cli.run方法(webpck-cli/lib/webpack-cli.js文件)
这个run方法比较长,看起来应该是在解析命令吧,然后经过漫长的解析后(这个过程不了解),解析到当前命令为build,接下来执行run方法中的第1039行代码loadCommandByName,在里面会先执行
this.webpack = await this.loadWebpack(),
await this.runWebpack(options, isWatchCommandUsed)
接下来,分别介绍loadWebpack和this.runWebpack
this.loadWebpack方法(webpack-cli/lib/webpack-cli.js)
async loadWebpack(handleError = true) {
return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError);
}
在tryRequireThenImport内部会执行require("webpack"),即加载webpack模块,那么其实this.webpack即为加载的webpack模块。根据webpack的package.json的描述信息知,webpack的入口文件为webpack/lib/index.js.
注意看lib/index.js文件,该文件会懒加载lib/webpack.js,只有在真正执行this.webpack()时,才会加载webpack.js文件
this.runWebpack (webpack-cli/lib/webpack-cli.js)
runWebpack在这里会调用this.createCompiler方法,返回结果为我们经常说的贯穿整个webpack生命周期的compiler(好像不对),接下来进入createCompiler内部。
createCompiler内部 (webpack-cli/lib/webpack-cli.js)
- 先拿到运行webpack需要的配置信息
- 运行
this.webpack,执行webpack/lib/webpack.js文件第108行代码
进入lib/webpack.js内部,这里不截出全部代码,只把关键截取出来
// compiler的创建方法
// create也只截取部分代码
const create = () => {
// 省略代码
/** @type {MultiCompiler|Compiler} */
let compiler;
let watch = false;
/** @type {WatchOptions|WatchOptions[]} */
let watchOptions;
if (Array.isArray(options)) {
// 省略代码
} else {
const webpackOptions = /** @type {WebpackOptions} */ (options);
/** @type {Compiler} */
compiler = createCompiler(webpackOptions);
watch = webpackOptions.watch;
watchOptions = webpackOptions.watchOptions || {};
}
return { compiler, watch, watchOptions };
};
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;
从以上代码看出,关键点是compiler = createCompiler(webpackOptions);和compiler.run(),接下来继续分析以上两个方法。
createCompiler内部(webpack/lib/webpack.js)
const createCompiler = rawOptions => {
// 将用户传入配置信息与默认信息组合
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
// Complier: lib/Compiler.js
const compiler = new Compiler(options.context, options);
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
// 遍历插件,并执行每个插件的apply或者call方法,compiler为入参
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
// 执行compiler的钩子函数
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 注意这里很重要
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
在
createCompiler主要做了
new了一个Compiler对象plugin.apply注册了所有的插件- 调用了
environment和afterEnvironment环境hook- 调用
new WebpackOptionsApply().process将配置属性转为plugin注册- 返回
compiler
继续分析上面函数调用的new WebpackOptionsApply().process(options, compiler);(lib/WebpackOptionsApply.js),内部实现有点长(大概就是根据外部配置执行一些webpack内部定义的插件),就不具体看了,先关注这两行代码
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
由下面分析可知,在注册了
entryOption钩子之后,然后又立马触发该钩子,然后在钩子内部回调用,为每个entry注册compilation钩子和make钩子,
进入EntryOptionPlugin函数具体看一下代码。
EntryOptionPlugin(lib/EntryOptionPlugin.js)
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
static applyEntryOption(compiler, context, entry) {
if (typeof entry === "function") {
// 代码省略
} 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);
}
}
}
}
接下来看EntryPlugin
apply(compiler) {
// 注册compilation钩子
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
// 注册make钩子
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
}
compiler.run内部(lib/Compiler 424行)
const run = () => {
// 运行beforeRun钩子
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
// 运行run钩子
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
};
先不管其他钩子的执行情况,先看this.compile(onCompiled)
compile(callback) {
const params = this.newCompilationParams();
// 触发beforeCompile钩子
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
// 触发compile钩子
this.hooks.compile.call(params);
// 出现compilation了
const compilation = this.newCompilation(params);
const logger = compilation.getLogger("webpack.Compiler");
logger.time("make hook");
// 触发make钩子
this.hooks.make.callAsync(compilation, err => {
logger.timeEnd("make hook");
if (err) return callback(err);
logger.time("finish make hook");
// 触发finishMake钩子
this.hooks.finishMake.callAsync(compilation, err => {
logger.timeEnd("finish make hook");
if (err) return callback(err);
process.nextTick(() => {
logger.time("finish compilation");
compilation.finish(err => {
logger.timeEnd("finish compilation");
if (err) return callback(err);
logger.time("seal compilation");
compilation.seal(err => {
logger.timeEnd("seal compilation");
if (err) return callback(err);
logger.time("afterCompile hook");
this.hooks.afterCompile.callAsync(compilation, err => {
logger.timeEnd("afterCompile hook");
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
});
});
}
从上面代码this.compile可以看出,在触发beforeCompile和 compile钩子之后,会触发make钩子。从EntryPlugin的分析中得知,注册了make钩子。下面具体来看看make钩子里面做了什么事。
那接下来主要看看
compilation.addentry做了什么
webpack/lib/Compilation.js
在执行
compilation.addentry这里主要做了
- 执行
_addEntryItem,用于添加入口的item - 执行
addModuleTree - 在
addModuleTree中,执行handleModuleCreation - 在
handleModuleCreation中执行factorizeModule,添加hooks到factorizeQueue队列中 - 在
factorizeModule回调用执行addModule,添加module模块到addModuleQueue队列中 - 在
addModule中执行AsyncQueue.js中的add,在add方法中执行root._ensureProcessing,再执行this._startProcessing(entry),再执行this._processor(entry.item, callbak(略));(在构造函数中,processor会转成_processor) buildQueue有一个属性processor值为this._buildModule.bind(this),再执行this._processor,即执行this._buildModule_buildModule中执行module.needBuild判断模块是否需要重新构建- 在
module.needBuild回调中执行module.build - 最后会在
webpack/lib/NormalModule.js中执行build方法,开始构建模块
module的build阶段
上一节中提到,module最后在webpack/lib/NormalModule.js中执行build方法,开始构建模块,那我们具体看看build到底做了哪些事情
webpack/lib/NormalModule.js
- 执行build,调用
_doBuild - 执行
_doBuild
这里的
const { getContext, runLoaders } = require("loader-runner");,在runLoaders的回调中执行processResult
- 执行
processResult
在
processResult中会执行callback,而这个callback即为调用_doBuild传入的callBack。
- 执行
_doBuild的callback
这个parse来自
node_modules/webpack/lib/javascript/JavascriptParser.js内的parse.这个parse其实是用到了acorn这个库来解析javascript
- 在
_doBuild的callback中调用handleParseResult,在handleParseResult中又调用handleBuildDone,在handleBuildDone里面又调用了传入build里的回调
在
build执行完后,又会执行传入_buildModule的回调,即为:
(node_modules/webpack/lib/util/AsyncQueue.js)
- 当
_buildModule的回调全部执行完后,hooks.make执行完成,于是接下来会执行webpack/lib/Compiler.js的compilation.finish和compilation.seal方法