一、前文回顾
上文详述了 compilation 对象的创建过程,期间讨论了以下内容:
- Compilation 类型的信息,包括定义文件模块、参数等;
- Compilation 的构造函数的主要逻辑:
- 2.1 compilation.hooks 定义;
- 2.2 compilation.mainTemplate 的作用------生成主入口 chunk 的模板;
- 2.3 compilation.chunkTemplate 的作用------生成非入口 chunk 的模板;
- 2.4 compilation.runteimTemplate 的作用 ------ 生成 webpack 运行时相关代码(runtime)模板;
- 2.5 compilation.moduleTemplate 的作用 ------ 生成 module 相关代码的模板;
- 2.6 模块构建的四队列的作用,并简述了各个队列的 processor 函数的作用,另外大致讲述了这四个队列的协同关系;
从这篇文章开始,我们将进入到正式模块构建工作。这一切的开始源自 EntryOptionPlugin 注册的 EntryPlugin 被触发,在 EntryPlugin 中调用了 compilation.addEntry 方法。
而我们今天的重点从 compililation.addEntry 开始;
二、复习 EntryPlugin 的注册
我们面试常常回答 "webpack 会从入口
开始递归解析依赖"。
那这个 入口
到底怎么加入到共建流程中的呢?前面我们在说 compiler 的创建过程中其实已经大致说过,这里为方便后面的行文,这里进行详细讲解。
2.1 WebpackOptionsApply
在创建 Compiler 的实例过程中,有一个重要步骤实例化 WebpackOptionsApply 并调用 process 方法实 例化,在该方法中会实例化 EntryOptionPlugin
js
new WebpackOptionsApply().process(compiler, options);
class WebpackOptionsApply {
constructor () {}
process () {
// 注册 EntryOptionPlugin
new EntryOptionPlugin().apply(compiler);
}
}
2.2 EntryOptionPlugin
EntryOptionPlugin 的作用是注册 compiler.hooks.entryOption 钩子,当钩子被触发时,调用 EntryOptionPlugin.applyEntryOption 方法注册 DynamicEntryPlugin 或者 EntryPlugin
js
class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
// 调用静态方法 applyEntryOption
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
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)) {
for (const entry of desc.import) {
new EntryPlugin(context, entry, options).apply(compiler);
}
}
}
}
}
2.3 EntryPlugin
EntryPlugin 主要做了两件事;
-
订阅 compiler.hooks.compilation 钩子,触发时设置 EntryDependency 的 ModuleFactory normalModuleFactory 工厂。这个用于创建入口模块用,后面讲;
-
订阅 compiler.hooks.make 钩子,触发时调用 compilation.addEntry 方法,将入口模块加入到 factorizeQueue 队列中,如此依赖就启动了以入口模块为起点的模块构建流程;
js
class EntryPlugin {
constructor(context, entry, options) {}
apply(compiler) {
// 1.
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
// 2.
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);
});
});
}
}
2.4 EntryPlugin 的触发
在调用 compiler.run() -> compiler.compile() 方法时会触发 compiler.hooks.make 钩子,进而触发 EntryPlugin 钩子;
js
class Compiler {
compile(callback) {
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
this.hooks.make.callAsync(compilation, err => {})
})
}
}
三、compilation.addEntry
结合前面的对 EntryPlugin 的复习,我们得到了这样一条从 npm run build 到 compilation.addEntry 的主线剧情
text
$: npm run build
-> webpack
-> webpack-cli
-> compiler.run()
-> compiler.compile()
-> compiler.hooks.make.callAsync()
-> EntryPlugin 被触发
-> compilation.addEntry()
到这里我们开始进入 compilation.addEntry 方法的讲解:
js
class Compilation {
addEntry(context, entry, optionsOrName, callback) {
const options =
typeof optionsOrName === "object"
? optionsOrName
: { name: optionsOrName };
this._addEntryItem(context, entry, "dependencies", options, callback);
}
_addEntryItem(context, entry, target, options, callback) {}
}
3.1 方法参数:
- context: 上下文目录,构建中就是当前项目的项目目录;
- entry: 入口对象,结合 EntryPlugin 可以看到传入的是由 EntryPlugin.createDependency 方法返回值对象;
- optionsOrName: 选项或者名字;
- callback: entry 处理的回调函数,用于和 compiler 通信进行后续的流程;
以下为调用时的实参:
3.2 方法逻辑
方法内部其实很简单,只做了两件事:
- 标准化处理 options,根据 optionsOrName 类型格式化成不同对象;
- 调用 compilation._addEntryItem() 方法;
下面我们开始进入compilation._addEntryItem 方法
四、compilation._addEntryItem
js
class Compilation {
_addEntryItem(context, entry, target, options, callback) {
const { name } = options;
let entryData =
name !== undefined ? this.entries.get(name) : this.globalEntry;
if (entryData === undefined) {
entryData = {
dependencies: [],
includeDependencies: [],
options: {
name: undefined,
...options
}
};
entryData[target].push(entry);
this.entries.set(name, entryData);
} else {
// 这里不成立,先忽略
}
this.hooks.addEntry.call(entry, options);
this.addModuleTree(/* ... */);
}
}
4.1 参数
- context: webpack 构建上下文目录,及项目目录
- entry: 入口对象,上文中 EntryPlugin.createDependency() 返回的 EntryDependeny 类型的实例对象;
- target: 目标类型,用于在 entryData 中分类类型进行缓存的标识;
- options: webpack 配置对象或者 nameOptions 对象,由 compilation.addEntry 标准化;
- callback: 回调函数,这个回调是 compilation.addEntry 接收到 callback,同理用于和 compiler 通信用;
下图为该方法执行时接到的实参图片:
4.2 逻辑
该方法执行分以下几个步骤:
- 根据 options 中是否有 name 属性从 compilation.entries 或者 compilation.globalEntry 中尝试获取缓存的入口 entryData;
- 根据上一步的 entryData 是否获取成功进行后续操作,这里我们重点关注没有获取到的情况(因为初次构建时没有缓存);
- 当没有 entryData 时,我们构建 entryData 对象,并将其缓存到 compilation.entries,另外 entryData.dependency 设为原始 entry 入口对象;即入口数据的依赖为 entry ;
- 触发 this.hooks.addEntry 钩子,传入 entry 和 options 对象;
- 调用 this.addModuleTree 方法;
4.3 _addEntryItem 的回调
这里的回调是指 _addEntryItem 调用时接收到的回调函数:
js
addEntry(context, entry, optionsOrName, callback) {
this._addEntryItem(context, entry, "dependencies", options, callback);
}
其实这里可以很清楚的看到 callback 来自 addEntry 方法,这个 callback 就是在 EntryPlugin 调用 compilation.addEntry 时传入的 callback;
五、compilation.addModuleTree
5.1 方法参数(或参数已经结构的属性)
- context:上下文,当前项目的目录;
- dependency:依赖对象,处理入口时就是 EntryPlugin 中创建的 入口依赖 EntryDependency 了;
- contextInfo:上下文信息,解析入口传入的 undefined;
以下是解析入口时的实参:
5.2 方法逻辑
- 该方法也用户根据用户传入的 depencency 获取对应的 moduleFactory;
- 然后组织 moduleFactory 和 dependency 等信息传给 compilation.handleModuleCreation 方法;而 ompilation.handleModuleCreation 方法内部会调用 factorizeModule 方法执行模块
及其依赖的子模块
的创建、构构建工作;
js
class Compilation {
// ...
addModuleTree({ context, dependency, contextInfo }, callback) {
const Dep = dependency.constructor;
const moduleFactory = this.dependencyFactories.get(Dep);
this.handleModuleCreation(
{
factory: moduleFactory,
dependencies: [dependency],
originModule: null,
contextInfo,
context
},
(err, result) => {
if (err && this.bail) {
} else if (!err && result) {
callback(null, result);
} else {
callback();
}
}
);
}
}
5.3 addModuleTree 回调
严格来讲,addModuleTree 回调并不属于 addModoleTree 方法,但是在这里说这个事儿会更清晰;
js
this.addModuleTree(
{
// .... 参数对象
},
(err, module) => {
// 回调
this.hooks.succeedEntry.call(entry, options, module);
return callback(null, module);
}
);
回调函数中就干了两件事:
- 触发 compilation.hooks.succeedEntry 钩子,并传入 entry 对象等参数;
- 调用 callback,这个 callback 是前面 compilation._addEntryItem 调用时传递的;这个 callback 的作用是 addEntry 的时候收到用于和 compiler 通信的 (详见:compilation.addEntry 3.1 方法参数)
六、总结
本文开始进入到了 webpack 处理入口模块的创建工作,今天主要复习compilation 和 compiler 协同启动模块构建工作:
- 复用 EntryPlugin 的注册过程及关键 compiler.hooks.make,此时 EntryPlugin 调用 compilation.addEntry 方法传入入口;
- 学习了 compilation.addEntry 方法作用,其核心调用 compilation._addEntryItem 方法加入 entry 项;
- 学习了 compilation._addEntryItem 方法,其内部处理 entryData 并调用 compilation.addModuleTree 加入入口模块,入口模块相当于 ModuleTree 的 root 节点;
- 学习了 compilation.addModuleTree 方法内部则是调用 compilation._handleModuleCreation 方法开始处理入口模块及其依赖的子模块的创建工作;
那么下文呢,我们就要进入 compilation._handleModuleCreation 探寻 webpack 宇宙的模块创建星球!