再读 Webpack 源码,各个击破:从 Compiler 和 Compilation 说起
共 18613字,需浏览 38分钟
·
2021-12-16 06:19
前言
前端的发展,离不开打包工具的助推。关于打包工具种类繁多,其中 Webpack 作为佼佼者被广泛使用。通过配置选项和 Loader、Plugin,我们可以实现各种功能。那么 Webpack 内部到底是怎么运行,通过本系列的源码学习,希望能给笔者有一个完整的认识。
1、准备工作
1.1、工具及版本说明
首先,在阅读本文之前。我们先约定下本系列文章约定的源码版本,如下:
webpack 4.46.0
对于 Node 版本,笔者使用的 14 的大版本,当然为了方便调试,建议使用不低于 10 的大版本。另外,我们使用 VSCode 进行调试和代码阅读。
1.2、约定及说明
阅读源码是一个学习的过程,当然也是一个比较痛苦的过程,尤其是 webpack 这种使用 hooks 来完成主流程的库。在主要步骤中会很跳跃,这一定程度上增加了源码阅读的难度,本文会集中在几个关键阶段或部分:compiler 、compilation 、Tapable 和 Hook、Module及其处理器 Loader、Plugin。并对行数过长的方法进行代码删减,保留一些关键代码的方法来让读者对整体执行流程有一个整体认识。然后,再根据自己的需要,按图索骥的对感兴趣的部分进行深入阅读。
2、源码分析
2.1、Webpack 执行入口
首先我们找到程序的入口位置,直接看 webpack 包(这个 node_modules\webpack 目录下)的 package.json 文件,并找到部分内容:
"main": "lib/webpack.js",
"web": "lib/webpack.web.js",
"bin": "./bin/webpack.js",
其他我们都不看,直接关注 main 这个属性,这就是我们在 Node 环境中执行 webpack 的入口。那么,我们得到完整的 webpack 代码路径:
node_modules\webpack\lib\webpack.js
const webpack = (options, callback) => {
...
let compiler;
// 根据配置类型是数组还是对象,来创建 compiler 对象
if (Array.isArray(options)) {
...
} else if (typeof options === "object") {
// 一般情况下,module.export 出来就是单个配置。我们可以主要看这里的代码
// 首先,通过一个默认选项对象 WebpackOptionsDefaulter 对我们的配置项进行处理
options = new WebpackOptionsDefaulter().process(options);
// 创建 compiler 对象,该对象贯穿webpack的整个生命周期
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 重要!!!执行配置项里所有 plugin 对象的 apply 方法,如果是函数则执行该函数
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
// 执行完成环境配置步骤相关的 hooks
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 重要!!!process 方法将会添加更多的 plugin、初始化 resolver 以及执行 hooks
compiler.options = new WebpackOptionsApply().process(options, compiler);
}
if (callback) {
if (
options.watch === true ||
(Array.isArray(options) && options.some(o => o.watch))
) {
const watchOptions = Array.isArray(options)
? options.map(o => o.watchOptions || {})
: options.watchOptions || {};
// 运行 compiler,这里会开启监听模式,后面会针对这个进行分析,
return compiler.watch(watchOptions, callback);
}
...
// 运行 comiler,进行编译工作
compiler.run(callback);
}
return compiler;
};
exports = module.exports = webpack;
通过以上的代码,主要完成了以下几个工作:
•1、配置项处理•2、创建 compiler 对象•3、执行配置项里 plugins 的 apply 方法•4、执行环境配置相关的 hooks•5、执行 WebpackOptionsApply 的 process 方法,这个方法的代码我在下面讲解•6、执行 compiler 的 run 或 watch 方法,真正的执行编译的任务
2.2、来自 WebpackOptionsApply.process 的加餐
上一节中提到 WebpackOptionsApply 的 process 方法,这个方法很长,大概4、500行(包括注释部分)。我们会针对这个方法进行重点解释。
process(options, compiler) {
let ExternalsPlugin;
// 上来是一大堆的属性配置,主要是一些输入、输出的路径
compiler.outputPath = options.output.path;
……
// 最终打包出的代码运行的目标环境,做一些插件创建和 apply 方法执行
if (typeof options.target === "string") {
let JsonpTemplatePlugin;
let FetchCompileWasmTemplatePlugin;
……
switch (options.target) {
case "web":
// 针对web浏览器目标环境的处理
break;
case "node":
...
default:
throw new Error("Unsupported target '" + options.target + "'.");
}
}
……
// 如果是打包成一个库的配置处理
if (options.output.library || options.output.libraryTarget !== "var") {
const LibraryTemplatePlugin = require("./LibraryTemplatePlugin");
new LibraryTemplatePlugin(
options.output.library,
options.output.libraryTarget,
……
).apply(compiler);
}
// 对于引入的依赖包在打包中进行去除,希望通过cdn方式或其他方式的处理
if (options.externals) {
ExternalsPlugin = require("./ExternalsPlugin");
new ExternalsPlugin(
options.output.libraryTarget,
options.externals
).apply(compiler);
}
……
// sourcemap 的处理
if (
options.devtool &&
(options.devtool.includes("sourcemap") ||
options.devtool.includes("source-map"))
) {
// sourcemap 参数处理
const hidden = options.devtool.includes("hidden");
const inline = options.devtool.includes("inline");
……
// 插件处理
const Plugin = evalWrapped
? EvalSourceMapDevToolPlugin
: SourceMapDevToolPlugin;
new Plugin({
filename: inline ? null : options.output.sourceMapFilename,
moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
……
}).apply(compiler);
} else if (options.devtool && options.devtool.includes("eval")) {
……
}
// compiler.hooks.entryOption 的执行
compiler.hooks.entryOption.call(options.context, options.entry);
// 严格模式的插件,这个我们在下面可以看一下它的源码部分
new UseStrictPlugin().apply(compiler);
……
// 在 optimization 中开启 sideEffects 的处理
if (options.optimization.sideEffects) {
const SideEffectsFlagPlugin = require("./optimize/SideEffectsFlagPlugin");
new SideEffectsFlagPlugin().apply(compiler);
}
……
// 代码做 split
if (options.optimization.splitChunks) {
const SplitChunksPlugin = require("./optimize/SplitChunksPlugin");
new SplitChunksPlugin(options.optimization.splitChunks).apply(compiler);
}
……// 省略的这部分都和 module 和 chunk 相关
// 压缩配置
if (options.optimization.minimize) {
for (const minimizer of options.optimization.minimizer) {
if (typeof minimizer === "function") {
minimizer.call(compiler, compiler);
} else {
minimizer.apply(compiler);
}
}
}
if (options.performance) {
const SizeLimitsPlugin = require("./performance/SizeLimitsPlugin");
new SizeLimitsPlugin(options.performance).apply(compiler);
}
……
// compiler.hooks.afterPlugins 调用
compiler.hooks.afterPlugins.call(compiler);
if (!compiler.inputFileSystem) {
throw new Error("No input filesystem provided");
}
// compiler.resolverFactory.hooks 上挂一些钩子
compiler.resolverFactory.hooks.resolveOptions
.for("normal")
.tap("WebpackOptionsApply", resolveOptions => {
return Object.assign(
{
fileSystem: compiler.inputFileSystem
},
cachedCleverMerge(options.resolve, resolveOptions)
);
});
……
// compiler.hooks.afterResolvers 调用
compiler.hooks.afterResolvers.call(compiler);
return options;
}
ok,可以看到在进入到 compiler.run 之前,在这里又做了大量的工作。这里主要的工作集中在根据我们对于 webpack 的配置项进行插件的添加和 apply 方法的执行。我们在上面的代码讲解里有一个严格模式的插件,我们简单看下它的源码:
apply(compiler) {
compiler.hooks.compilation.tap(
"UseStrictPlugin",
(compilation, { normalModuleFactory }) => {
const handler = parser => {
parser.hooks.program.tap("UseStrictPlugin", ast => {
const firstNode = ast.body[0];
……// 一些处理代码
});
};
normalModuleFactory.hooks.parser
.for("javascript/auto")
.tap("UseStrictPlugin", handler);
……
}
);
}
这里它主要通过钩子方法,可以在对代码生成一颗抽象语法树 ast 之后,拿到 ast,并做一些工作。这个地方其实给了我们一些启示,能够针对 ast 进行一些操作,当然还可以使用 Babel 作为可选方案。
2.3、贯穿始终的 Compiler
Compiler 和 Compilation 是两个在 webpack 的 plugin 中常用的两个对象。二者也包含了很多的 hooks,这些 hooks 代表了 webpack 在执行过程的不同阶段。其中,通过前面的代码我们看到了 Compiler 的创建过程,它包含了很多的配置项 options,里面又包含了 Loader 和 Plugin 信息。其生命周期一直到 webpack 结束,几乎等同于 webpack 实例本身。
而 Compilation 到此为止,我们还没有看到。它其实只是一次编程过程,包含了 Module、编译后的输出等,同时他也拥有 Compiler 对象实例。
在上面对于 webpack 执行入口的源码分析中,我们看到了 compiler.watch 或 compiler.run 才真正执行起来。我们一般在开发阶段中会使用到 webpack-dev-server ,它这里 Watch 模式默认开启。本文要讲以 compiler.watch 方法作为入口继续源码的分析。
源码位置 node_modules\webpack\lib\Compiler.js
watch(watchOptions, handler) {
if (this.running) return handler(new ConcurrentCompilationError());
this.running = true;
this.watchMode = true;
……
return new Watching(this, watchOptions, handler);
}
前面的一些设置代码,我们可以忽略,直接看最后创建并返回了一个 Watching 对象。
class Watching {
constructor(compiler, watchOptions, handler) {
……
this.compiler = compiler;
this.running = true;
// 这里调用了 compiler.readRecords 方法,该方法比较简单,最后会执行回调方法
this.compiler.readRecords(err => {
if (err) return this._done(err);
// 最后还是来到 _go 方法
this._go();
});
}
_go() {
……
// 执行 compiler.hooks.watchRun
this.compiler.hooks.watchRun.callAsync(this.compiler, err => {
const onCompiled = (err, compilation) => {
// 实际上最终执行的是 compiler.hooks.emit
this.compiler.emitAssets(compilation, err => {
//
this.compiler.emitRecords(err => {
……
// 执行
return this._done(null, compilation);
});
});
};
// 在执行 compiler.hooks.watchRun 后,真正进入编译阶段,并传入 onCompiled 这个回调函数
this.compiler.compile(onCompiled);
});
}
_done(err, compilation) {
this.running = false;
if (err) {
// 出错之后,执行 compiler.hooks.failed
this.compiler.hooks.failed.call(err);
return;
}
// 编译正常完成,执行 compiler.hooks.done
this.compiler.hooks.done.callAsync(stats, () => {
if (!this.closed) {
// 监听,暂时略过
this.watch(
……
);
}
……
});
}
}
ok,到这里可能有点混乱了,我们来理一理流程主线:
**Compiler.watch() ---->> Watching._go() ---->> Compiler.hooks.watchRun ---->> Compiler.compile(onCompiled) ---->> onCompiled ---->> 执行 Compiler.hooks.emit 和 Watching._done() ---->> Compiler.hooks.done (通过Watching._done执行到)**。
这个主线里,我们重点要记住 Compiler.hooks.emit 和 Compiler.hooks.done。前者是即将进行 output,这是我们最后能够修改模块内容的机会,后者则是完成编译文件输出。另外,在这个主线里,我们看到缺了 Compiler.compile(onCompiled) 这一部分的内容。接下来,我们继续 Compiler 的源码部分
node_modules\webpack\lib\Compiler.js
class Compiler extends Tapable {
createCompilation() {
return new Compilation(this);
}
newCompilation(params) {
const compilation = this.createCompilation();
……
// 执行两个 hooks,至此,compilation 才真正被创建
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
createNormalModuleFactory() {
const normalModuleFactory = new NormalModuleFactory(……);
// 调用 hooks.normalModuleFactory
this.hooks.normalModuleFactory.call(normalModuleFactory);
return normalModuleFactory;
}
createContextModuleFactory() {
const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
// 调用 hooks.contextModuleFactory
this.hooks.contextModuleFactory.call(contextModuleFactory);
return contextModuleFactory;
}
newCompilationParams() {
// 主要是创建两个工厂实例
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()
};
return params;
}
compile(callback) {
// 拿到创建 Compilation 的参数
const params = this.newCompilationParams();
// 执行 hooks.beforeCompile
this.hooks.beforeCompile.callAsync(params, err => {
// 执行 hooks.compile,到这里为止,compilation 还未创建
this.hooks.compile.call(params);
// 创建 Compilation 对象
const compilation = this.newCompilation(params);
// hooks.make 进入编译阶段
this.hooks.make.callAsync(compilation, err => {
// compilation 部分
compilation.finish(err => {
compilation.seal(err => {
// 执行 hooks.afterCompile,这里已经完成编译的部分
this.hooks.afterCompile.callAsync(compilation, err => {
// 这里就是那个 onCompiled 回调最终被执行
return callback(null, compilation);
});
});
});
});
});
}
}
完成了上述代码的过程,我们对上面的 compiler.compile() 内部的流程主线可以归纳(侧重 hooks 部分): compiler.compile() ---->> compiler.hooks.normalModuleFactory 和 compiler.hooks.contextModuleFactory ---->> compiler.hooks.beforeCompile ---->> compiler.hooks.compile ---->> compiler.hooks.thisCompilation ---->> compiler.hooks.compilation ---->> compiler.hooks.make ---->> compilation.finish() ---->> compilation.seal() ---->> compiler.hooks.afterCompile ---->> onCompiled()
hooks 本质上反映的是整个 webpack 运行的不同阶段。在上面这个主线中,我们其实重点关注两个点:
•compiler.hooks.make,从这里开始正式进入编译•compilation.seal() 函数,它的执行标识着封闭编译,不再添加 module
然后我们看到从 compiler.hooks.make 直接到了 compilation.finish() 这一步。中间经历了什么,我们在下一节中进行源码分析。
另外,上述代码中生成了两个工厂对象:ContextModuleFactory 和 NormalModuleFactory 。其中,ContextModuleFactory 用来解析目录,为每个文件生成请求,并依据传递来的 regExp 进行过滤。最后匹配成功的依赖关系将被传入 NormalModuleFactory 。NormalModuleFactory 用来生成各类模块。从入口点开始,它会分解每个请求,解析文件内容以查找进一步的请求,然后通过分解所有请求以及解析新的文件来爬取全部文件。在最后阶段,每个依赖项都会成为一个模块实例。
2.4、大包干的 Compilation
在上一节的最后,我们缺了一大块内部,就是怎么就从 compiler.hooks.make 直接到了 compilation.finish() 这一步?开发过 plugin 的同学应该知道,通过 hooks 我们可以通过 tap 方法去钩住某个执行阶段,compiler.hooks.make 执行就会触发这些 hooks。我们通过调试或在 ./node_modules/webpack 下用关键词搜索 hooks.make.tap 可以发现很多插件。比如:DllEntryPlugin、MultiEntryPlugin、AutomaticPrefetchPlugin、PrefetchPlugin等,我们以 MultiEntryPlugin 为例来看一下它的源码实现:
node_modules\webpack\lib\MultiEntryPlugin.js
class MultiEntryPlugin {
apply(compiler) {
compiler.hooks.make.tapAsync(
"MultiEntryPlugin",
(compilation, callback) => {
const { context, entries, name } = this;
// 生成一个 MultiEntryDependency 依赖对象
const dep = MultiEntryPlugin.createDependency(entries, name);
// 调用了 compilation.addEntry 方法
compilation.addEntry(context, dep, name, callback);
}
);
}
static createDependency(entries, name) {
return new MultiEntryDependency(
entries.map((e, idx) => {
const dep = new SingleEntryDependency(e);
return dep;
}),
name
);
}
}
通过插件在 compiler.hooks.make 上挂的钩子,成功的从 compiler 转译到了 compilation 的执行阶段。compilation.addEntry(context, dep, name, callback) 方法从名字上,我们就可以看出是为编译添加入口,其实这并不难理解。接下来,我们看看 addEntry 方法之后都干了些什么:
node_modules\webpack\lib\Compilation.js
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name);
// 添加模块链接,其实就是从 entry 入口开始对每个模块 build,然后分析出依赖模型进行 build 的一个过程
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
//
this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
}
);
}
_addModuleChain(context, dependency, onModule, callback) {
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
// dependencyFactories 里有很多类型的工厂对象,通过依赖的具体类型拿到工厂对象,暂时忽略下
const moduleFactory = this.dependencyFactories.get(Dep);
this.semaphore.acquire(() => {
// 通过模块工厂对象创建模块,工厂的 create 方法,我们先放一下,在后面说。
moduleFactory.create({},
(err, module) => {
// 添加模块
const addModuleResult = this.addModule(module);
const afterBuild = () => {
if (addModuleResult.dependencies) {
// 处理依赖模块
this.processModuleDependencies(module, err => {});
}
};
if (addModuleResult.build) {
// 开始构建模块
this.buildModule(module, false, null, null, err => {
// 完成构建之后,继续后面有可能的依赖模块处理
afterBuild();
});
}
}
);
});
}
addModule(module, cacheGroup) {
// 缓存,这里根据模块的标识符进行查找
const identifier = module.identifier();
// _modules 里存放的是已经添加的模块信息
const alreadyAddedModule = this._modules.get(identifier);
if (alreadyAddedModule) {
return {
module: alreadyAddedModule,
issuer: false,
build: false,
dependencies: false
};
}
const cacheName = (cacheGroup || "m") + identifier;
if (this.cache && this.cache[cacheName]) {
// 这里面主要是针对缓存部分有更新的处理,需要对模块进行 reBuild 处理
}
return {
module: module,
issuer: true,
build: true,
dependencies: true
};
}
buildModule(module, optional, origin, dependencies, thisCallback) {
// 在模块构建开始之前触发,可以用来修改模块。
this.hooks.buildModule.call(module);
// 使用模块对象进行构建
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
error => {
// 执行成功构建的钩子
this.hooks.succeedModule.call(module);
return callback();
}
);
}
processModuleDependencies(module, callback) {
this.addModuleDependencies(
module,
sortedDependencies,
this.bail,
null,
true,
callback
);
}
addModuleDependencies(
module,
dependencies,
bail,
cacheGroup,
recursive,
callback
) {
asyncLib.forEach(
dependencies,
(item, callback) => {
semaphore.acquire(() => {
const factory = item.factory;
factory.create({},
(err, dependentModule) => {
const addModuleResult = this.addModule();
const afterBuild = () => {
if (recursive && addModuleResult.dependencies) {
this.processModuleDependencies(dependentModule, callback);
}
};
if (addModuleResult.build) {
this.buildModule(
err => {
afterBuild();
}
);
}
}
);
});
},
err => {
}
);
}
这一块涉及到 compilation 中的几个方法,源码部分很多,笔者删除了大部分。主线捋一捋大致如下:通过 addEntry 找到入口模块然后把模块和依赖模块进行构建,构建过程是先构建当前模块,该过程伴随着缓存的处理和更新,真正的构建是通过具体模块对象的 module.build 方法进行(这个后面系列的文章中分析)。构建完当前模块后对分析出来的依赖模型就行构建,不断重复该过程直至所有的模块构建完毕。在这个过程中伴随着一些 hooks 的执行。
那么回到之前的那个问题,现在我们知道了如何从 compiler.hooks.make 顺滑的走到了 compilation.addEntry ,最终完成模块的构建工作。单个模块完成构建工作后执行了 compilation.hooks.succeedModule 钩子。那么,webpack 如何确定所有的模块以及构建完成最终执行了 compilation.finish() 呢?这个问题,我们放到下一篇文章中去介绍 Tapable 和 Hook。另外,我们在上面的源码分析中留下了一个问题就是模块如何构建,这个在系列三中去解答。