原文发布在我的 GitHub 欢迎 star
收藏。
本文源码来源:webpack-4
编译之前
在开始编译流程之前,webpack
会处理用户配置,首先进行校验,通过后与默认配置合并或者是调用相应函数进行处理,输出最终的 options
。之后实例化 Compiler
,并传入 options
,并为注册的 plugins
注入 compiler
实例。如果配置了 watch
选项,则添加监听,在资源变化的时候会重新进行编译流程;否则直接进入编译流程:
const webpack = (options, callback) => {
// 校验
validateSchema(webpackOptionsSchema, options);
/* ... */
// 处理、合并 options
options = new WebpackOptionsDefaulter().process(options);
// 实例化 Compiler
const compiler = new Compiler(options.context);
/* ... */
// 注册插件
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
// 开始编译流程
if (watch) {
compiler.watch(watchOptions, callback);
} else {
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});
}
}
module 编译
在 compiler.run()
的过程中,会调用 compile
方法。在 compile
方法中会实例化 Compilation
,模块的编译构建流程都由 Compilation
控制。实例化 Compilation
后紧接着触发 compilation
和 make
事件,从 make
开始主编译流程。这里有一个很细节的点,其实也与 webpack
的插件机制有关,webpack
的插件机制固然让其有很好的扩展性,但对于阅读源码来说会把人绕晕,众多的钩子,注册钩子函数的代码与触发的地方很难让人找到其联系之处,只能通过编辑器的全局搜索去找到注册的钩子函数。在处理 options
的时候,会调用到 EntryPlugin
插件,其内部会注册 compilation
与 make
事件:
apply(compiler) {
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
// 设置 DependencyFactories,在添加 moduleChain 的时候会用上
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
const { entry, name, context } = this;
const dep = EntryPlugin.createDependency(entry, name);
// 编译文件模块的入口
compilation.addEntry(context, dep, name, err => {
callback(err);
});
});
}
addEntry
在 make
事件触发后,会调用 compilation.addEntry
从入口文件开始,根据 dep
类型判断使用哪一种 moduleFactory
,并且将入口添加到 _preparedEntrypoints
属性中,其结构如下:
const slot = {
name: name, // entry 的 name
request: null, // entry 的 request
module: null // 对 entry 文件解析成的最终 module,在 afterBuild 中对其赋值
};
// ...
this._preparedEntrypoints.push(slot);
调用 _addModuleChian
将模块加入编译链条,会传入一个 dependency
对象,存有 entry
的 name
和 request
等信息。request
中,存储的是文件的路径信息:
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name);
// ...
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
// ...
}
);
}
createModule
在 _addModuleChain
的回调中会调用到 xxxModuleFactory
的 create
方法。以 normalModuleFactory
的 create
方法为例,先是触发 beforeResolve
事件,然后触发 normalModuleFactory
的 factory
事件,并调用返回的 factory
方法:
create(data, callback) {
// ...
this.hooks.beforeResolve.callAsync({ ... },
(err, result) => {
// ...
const factory = this.hooks.factory.call(null);
// ...
factory(result, (err, module) => {
// ...
});
}
);
}
在 factory
方法中,会触发 resolver
事件,并返回一个函数,通过调用此函数执行文件路径及相关 loader
路径的解析,完成后触发 afterResolve
事件,并在其回调中生成一个 module
实例,并将 resolve
的结果存入其中:
// factory 事件注册
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
let resolver = this.hooks.resolver.call(null);
// ...
resolver(result, (err, data) => {
// ...
this.hooks.afterResolve.callAsync(data, (err, result) => {
// ...
let createdModule = this.hooks.createModule.call(result);
if (!createdModule) {
if (!result.request) {
return callback(new Error("Empty dependency (no request)"));
}
// normalModule 实例
createdModule = new NormalModule(result);
}
createdModule = this.hooks.module.call(createdModule, result);
return callback(null, createdModule);
});
});
});
// resolve 事件注册
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
// ...
// loader 和 normal 文件的 resolve 有所区别
const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);
// resolve loader & normal path
});
buildModule
回到 normalModuleFactory.create
的 callback
中,会依次执行 addModule
---> addReason
---> buildModule
---> afterBuild
。
-
addModule: 将前面创建的
module
实例添加到全局Compilation.modules
数组中和_modules
对象中; -
addReason: 为该
module
添加reason
,即是哪个module
依赖了该module
; -
buildModule: 编译
module
; -
afterBuild: 处理依赖,递归编译
module
;
调用 buildModule
,最终会走到相应 module
实例的 build
方法中,前面我们的 module
是 NormalModule
的实例,所以我们来看看 NormalModule
的 build
方法:
// NormalModule 的 build 方法
build(options, compilation, resolver, fs, callback) {
// ...
return this.doBuild(options, compilation, resolver, fs, err => {
// doBuild 回调
// ...
try {
// 进行 AST 的转换
const result = this.parser.parse(
// 如果在 runLoaders 的时候已经解析成 AST 则使用 _ast,否则传入 JS 源代码
this._ast || this._source.source(),
{ current: this, module: this, compilation: compilation, options: options },
(err, result) => {
if (err) handleParseError(err);
else handleParseResult(result);
}
);
if (result !== undefined) handleParseResult(result);
} catch (e) {
handleParseError(e);
}
});
}
// doBuild 方法
doBuild(options, compilation, resolver, fs, callback) {
// ...
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
// ...
// createSource 将 loaders 处理的结果转换为字符串
this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source),
resourceBuffer,
sourceMap
);
this._sourceSize = null;
// 如果已经转换为 AST,则存在 _ast 中
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
return callback();
}
);
}
处理依赖
在 duBuild
方法中,会进行 loaders
的转换。在 this.parser.parse
方法中,会将转换后的源码进行 AST
转换,并在 import/export
等地方添加相应的 Dependency
,也可以理解为某种占位符,在后续的解析中,会根据相应的模板等进行替换生成最终文件。buildModule
结束后,执行其回调,并调用 afterBuild
方法,在 afterBuild
方法中,调用了 processModuleDependencies
方法处理依赖:
processModuleDependencies(module, callback) {
const dependencies = new Map();
// 省略对 dependencies 做的一些处理
const sortedDependencies = [];
// 将依赖解析成下面的结构
for (const pair1 of dependencies) {
for (const pair2 of pair1[1]) {
sortedDependencies.push({
factory: pair1[0],
dependencies: pair2[1]
});
}
}
// 添加模块依赖的解析
this.addModuleDependencies(
module,
sortedDependencies,
this.bail,
null,
true,
callback
);
}
// addModuleDependencies 遍历依赖,并重新进行 module 的编译
addModuleDependencies(module, dependencies, bail, cacheGroup, recursive, callback) {
const start = this.profile && Date.now();
const currentProfile = this.profile && {};
asyncLib.forEach(
dependencies,
(item, callback) => {
const dependencies = item.dependencies;
// ...
semaphore.acquire(() => {
const factory = item.factory;
// create
factory.create(
{
// ...
},
(err, dependentModule) => {
// ...
const iterationDependencies = depend => {
for (let index = 0; index < depend.length; index++) {
// ...
// addReason
dependentModule.addReason(module, dep);
}
};
// addModule
const addModuleResult = this.addModule(
dependentModule,
cacheGroup
);
// ...
if (addModuleResult.build) {
// buildModule
this.buildModule(
dependentModule,
isOptional(),
module,
dependencies,
err => {
// ...
// afterBuild
afterBuild();
}
);
}
// ...
}
);
});
},
err => {
// ...
}
);
}
可以看到,在 addModuleDependencies
方法中,对前一个 module
的依赖进行遍历,又重新执行 factory.create
---> addModule
---> buildModule
---> afterBuild
,进行编译。当所有的依赖都编译完,便完成了 module
的编译过程。回到 make
事件的回调中,此时所有需要编译的 module
都已经经过上面的步骤处理完毕,接下来会调用 compilation.seal
进行 chunk
图的生成工作:
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
// seal 阶段,组合 chunk,生成 chunkGroup
compilation.seal(err => {
if (err) return callback(err);
// 在其回调中触发 afterCompile 事件,编译过程结束
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
生成 chunkGroup
未完待续...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。