webpack 深入浅出之 —— compiler.compile
一、前文回顾
上篇小作文完成了启动编译流程的 compiler.run 方法的详细内容讲解,所谓启动,其实是整个编译流程的统揽,看完这篇你就可以大大方方的告诉面试官,webpack 编译大致流程了。
- compiler.run 方法内部封装 run 方法,run 来负责具体的工作;
- run 方法首先触发 hooks.beforeRun,触发 NodeEnvironmentPlugin 和 ProgressPlugin
- 接着触发 hooks.run 钩子,暂无插件;
- 调用 compiler.readRecords 方法读取 records 文件,并介绍了 compiler._readRecords 方法的具体实现;
- 调用 compiler.compile 方法开启编译流程;
- 介绍了处理 compiler.compile 编译结果的 onCompiled 回调;
- 介绍了处理最终编译结果并负责和 webpack-cli 通信的 finalCallback 回调函数;
代码执行肯定是个 深度优先
的事儿,但是我这里是个广度优先
。
今天我们进入下一个重要环节——编译工作开始!前面我们叙述了很多的内容,都属于准备阶段从这里开始就进入正题了。
而实现这些工作的方法是 compiler.compile 方法,下面我们详细的学习一波~
二、compiler.compile
- 方法位置:webpack/lib/Compiler.js -> Compiler.prototype.compile
- 参数:callback,也就是上一篇小作文里的 onCompiled,该方法主要处理编译产物bundle是否写入及肯定情况下的文件写入工作;
2.1 Compiler.prototype.compile 方法总览
class Compiler {
// ...
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
this.hooks.finishMake.callAsync(compilation, err => {
process.nextTick(() => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
});
});
});
});
});
});
}
}
下面我们快速浏览一下,看看 compile 方法都做了什么工作:
- 调用 compiler.newCompilationParams() 获取创建 compilation 实例对象的参数;
- 传入上一步获取的 param 对象,触发 compiler.hooks.beforeCompile 钩子;
- hooks.beforeCompile 钩子结束后触发 compiler.hooks.compile 钩子,参数同样传入的是第一步获取的 params 对象;
- 调用 compiler.newCompilation(params) 创建 compilation 对象;
- 传入 compilation 对象触发 compiler.hooks.make 钩子;
- compiler.hooks.make 解释后触发 compiler.hooks.finishMake 钩子;
- compiler.hooks.finishMake 触发结束后调用 compilation.finish;
- compilation.finish 调用结束后调用 compilation.seal 方法;
- 在 compilation.seal 方法结束后触发 compiler.hooks.afterCompile;
- 最后触发 compiler.compile 的回调函数 callback,也就是上文说的 onCompiled 函数;
2.2 compiler.newCompilationParams() 方法
class Compiler {
// ...
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory()
};
return params;
}
}
该方法用于获取 创建 compilation 对象的参数,这里面主要初始化了两个模块工厂类:
- NormalModuleFactory: webpack 中的常规模块工厂类;
- ContextModuleFactory: 上下文模块的工厂类;
这里需要先解释两个概念:
- 普通模块:所谓常规模块和大家认知的 ESM 或 CommonJS 或 CMD/AMD 的模块规范产物,粗暴理解就是 JS 模块;
- 上下文模块:webpack 支持一种叫做上下文的模块。当编译时无法确定具体某一个模块时,于是 webpack 直接引用一个目录,把目录中的所有模块都打包了,然后等运行时去具体到某一个模块,这种就叫做上下文模块,比如
const someModuleDir = fetch('some.domain.com/x-pai/get-js-module');
for (const k of someModuleDir) {
require('../some-ctx-dir/' + k)
}
这种情况下,webpack 为了确保代码能正常工作,webpack 会把 some-ctx-dir 目录下的所有模块都打包;
没看懂对不对,这个也确实不用懂,现在业内主流逻辑是通过静态分析,在编译时就把不必要的东西移除掉;
这种场景纯科普用,不用重点关注,后面我们会自动忽略掉有关 ContextMoudle 的所有处理逻辑;
2.2.1 compiler.createNormalModuleFactory() 方法
class Compiler {
// ...
createNormalModuleFactory() {
const normalModuleFactory = new NormalModuleFactory({});
this.hooks.normalModuleFactory.call(normalModuleFactory);
return normalModuleFactory;
}
}
从这里可以清晰看到,createNormalModuleFactory 方法返回了 NormalModuleFactory 这样一个实例对象;
NormalModuleFactory 类型的主要作用在模块的创建和构建过程中,主要做了以下工作:
- 定义 factory/resolve 等 NMF 与模块创建相关生命周期钩子;
- 格式化 loader 的规则信息,我们在 webpack.config.js 中声明 loader 的方式多种多样,这里有人给你做标准化;
- 定义 loader 和 模块的加载解析工作;
- 定义模块构建相关工作;
更多具体工作我们等到模块的构建工作中进一步展开,现有一个基本的认识即可;
2.2.2 compiler.createContextModuleFactory() 方法
这里的作用于上面的 NMF 类似,不做过多展开;
2.3 compiler.hooks.beforeCompile.call
触发 compiler.hooks.beforeCompile 钩子,有几个插件订阅在这个阶段:
- DllReferencePlugin:DLL 引用插件,即动态链接库的引用库,在 beforeCompile 阶段尝试获取 manifest.json,即 DLL 清单列表;
- ProgressPlugin:上文讨论过,该插件的作用是在 Terminal 中输出 webpack 构建进度;
- LazyCompilationPlugin:用于在webpack beforeCompile 中针对一些懒加载的入口进行后处理,这不是我们的主线剧情,无须过度关注;
2.4 compiler.hooks.compile
触发 compiler.hooks.compile 钩子,有几个插件订阅在这个阶段:
- DllReferencePlugin:在 compiler.hooks.compile 阶段根据配置选项生成 externals 对象,并将其应用到正常的模块工厂。这可以看出,Dll 的引用基于 External 实现类似;
- ExternalsPlugin:处理 external 选项,在该阶段为配置的各个 external 配置 ExternalModuleFactoryPlugin,该插件订阅 NMF 的 factory 阶段为配置的外部模块创建模块,以便这些外部模块可以正常的加入的 webpack runtime 的工作流中;
2.5 compiler.newCompilation(params)
创建 compilation 对象,该方法接收前面的 params 对象即 { normalModuleFactory, contextModuleFactory };
class Compiler {
// ...
newCompilation(params) {
const compilation = this.createCompilation(params);
compilation.name = this.name;
compilation.records = this.records;
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
}
创建 compilation 的过程是由 compiler.createCompilation 方法完成:
class Compiler {
newCompilation (params) {}
// ....
createCompilation(params) {
this._cleanupLastCompilation();
return (this._lastCompilation = new Compilation(this, params));
}
}
compilation 是 webpack 中最重要的对象,相当于是处理具体的编译相关事宜的对象。
包括模块的构建、生成 moduleGraph、chunkGraph、以及模块、chunk 的相关优化的工作都是由 compilation 直接或者间接完成(调度生命周期交由具体的插件)完成的。
这些工作,我们后面深度展开,先大致有个印象即可;
2.6 compiler.hooks.make
有了 compilation 之后,触发 compiler.hooks.make 钩子,从这里开始就即将进入模块的构建阶段了。这是因为我们前面在说 WebpackOptionsApply.prototype.process 方法的时候提及的一件事 ——EntryOptionPlguin 的注册。
EntryOptionPlguin 中为每个 entry 注册了 EntryPlugin,而 EntryPlugin 又订阅了 compiler.hooks.make 钩子:
2.6.1 EntryOptionPlugin
EntryOptionPlugin 订阅 compiler.hooks.entryOption 钩子,当钩子触发时调用 EntryOptionPlugin.applyEntryPlugin 根据不同类型的入口应用不同类型的入口插件;
- entry 为 function 类型则说明是动态入口,则应用 DynamicEntryPlugin 入口插件;
- 否则就是普通的入口,则应用 EntryPlguin 入口插件;
class EntryOptionPlugin {
apply(compiler) {
// 订阅 compiler.hooks.entryOpion 钩子
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
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 {
// !!!! 这里是重点,这个就是我们常规的配置了
// 初始化 EntryPlugin 插件初始化普通入口
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 都做了哪些工作
2.6.2 EntryPlugin
现在我们看看这个大家口中的入口到底是什么实现的,它又默默做了哪些工作呢?
class EntryPlugin {
constructor(context, entry, options) {
this.context = context;
this.entry = entry;
this.options = options || "";
}
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);
});
});
}
}
总结下来一共就两个点:
- 订阅 compiler.hooks.compilation 钩子,向 EntryDependency 设置工厂类为 normalModuleFactoy;
- 创建入口依赖并订阅 compiler.hooks.make 钩子,该钩子的回调则是调用 compilation.addEntry 方法正式向 compilation 这个“加工厂” 投放 “原材料”;
当然后面的工作就是围绕着 compilation 进行展开了,后面咱们再深入了解 compilation 对象;
三、总结
本文对 Compiler.prototype.compile 方法进行了由浅入深的解析,compile 则是 compiler 对象真正处理编译工作的地方,因此 compiler.compile 方法的内部就是整个编译过程的调度图。
注意这里称呼“调度员”,这和机场的调度塔塔是一样的,它不负责具体的的航班的起飞、降落等,它只是发布各种指令,然后地勤、机组人员共同完成具体的起降工作。
回过头来,compiler.compile 方法主要做了以下工作:
- 调用 compiler.newCompilationParams() 方法为创建 compilation 实例准参数;
- compiler.hooks.beforeCompile 钩子;
- 触发 compiler.hooks.compile 钩子,参数同样传入的是第一步获取的 params 对象;
- 执行 compiler.newCompilation(params) 创建 compilation 实例;
- 传入 compilation 对象触发 compiler.hooks.make 钩子;
- 接着介绍了订阅在 compiler.hooks.make 上的 EntryPlugin,同时介绍了 EntryPlugin 为整个构建投料的重要意义。
后面相当一段时间的重点将会转移到 compilation 对象上,这是由于具体的工作将要展开了,比如模块的编译、执行 loader,创建模块图等等。
转载自:https://juejin.cn/post/7337309863158300687