一、前文回顾
上文详细讨论了 Compiler 的构造函数中的重要细节工作,包括以下几点:
- 调度整个构建过程的 compiler.hooks 的注册,并且挑选其中具有代表性的钩子进行了讲解,包括其类型、参数、调用时机、作用等;
- 介绍了 webpack 的四个文件系统包括,负责输入、输出、中间产物、watch 四种文件系统;outputFileSystem:用于向文件系统写入的场景;intermediateFileSystem:这个属于 webpack 特有的文件系统,负责 webpack 构建过程中的中间产物,比如持久化缓存的读写工作;inputFileSystem:用于 webpack 文件的读场景;
- watchFileSystem:该文件系统封装了监听文件变化的能力,用于 watch 模式下的监听文件变化的文件系统;
- records 对象的作用和相关输入和输出路径;
- resolverFactory 的实例化以及 resolverFactory 的作用;
- 持久化缓存的声明及 compiler.cache、IdleFileCachePlugin、PackFileSCachetrategy 三者间的关系;
从本文开始正式进入到了编译阶段,进入的标志是 compiler.run 方法的调用,本文将详细讨论 run 方法的工作;
二、回顾编译流程启动
前面在介绍创建 Compiler 的过程中提及过,调用 createCompiler 方法后得到 compiler 对象,接着就是根据是有 watch 选项选择调用 compiler.watch() 或者 compiler.run() 方法;
js
const webpack = (options, callback) => {
const create = () => { /*创建 compiler 的逻辑 */ };
const { compiler, watch, watchOptions } = create();
// 启动编译流程
if (watch) {
compiler.watch(watchOptions, callback);
} else {
compiler.run((err, stats) => {});
}
return compiler;
);
watch 模式相关内容我们后面用专门的篇幅去讲,这里我们不做讨论,我们的中心放在一次性编译的启动方法 compiler.run()
三、compiler.run 方法
该方法来自 Compiler.prototype.run 方法:
- 方法定义位置:webpack/lib/Compiler.js -> Compiler.prototype.run
- 参数:callback,受理编译结论的回调函数;
- 调用实参:webpack/lib/webpack.js 调用时 :
compiler.run((err, stats) => { /*这个就是 callback */ });
3.1 整体结构
js
class Compiler {
constructor () {}
run(callback) {
if (this.running) {
return callback(new ConcurrentCompilationError());
}
const finalCallback = (err, stats) => {};
this.running = true;
const onCompiled = (err, compilation) => { };
const run = () => {};
if (this.idle) {
this.cache.endIdle(err => {
this.idle = false;
run();
});
} else {
run();
}
}
}
经过剥离后,我们可以看到该方法内做了以下内容:
- 判断 compiler.running 状态,防止重复执行相同编译任务,若处于执行状态(this.running 为 true)则抛出异常终止本次调用;
- 声明 finalCallback 这个作为编译流程的最终回调,持久化缓存的写入信号就是在这里释放的;
- 设置 compiler.running 为 true;
- 声明内部 run 方法,该方法封装了启动的具体工作;
- 声明 onCompiled 内部方法,用于处理编译过程中的事件回调,根据编译的状态和钩子函数的返回值执行不同的操作;
- 判断当前的空闲状态:compiler.idle,该标识在持久化缓存写入的时候为 true,根据该状态不同有以下处理:
- this.idle 为 true,则需要等到缓存处理结束的回调里调用 run() 方法启动编译;
- this.idle 为 false,则直接调用 run 方法;
3.2 run 方法
这个私有的 run 方法作为启动入口,我们先来研究它,先来看下整体:
js
const run = () => {
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
};
该方法的整体结构还是比较简单的,很清晰,是个回调的嵌套,很显然是个明确了先后顺序的事儿:
3.2.1 compiler.hooks.beforeRun.call
触发 compiler.hooks.beforeRun 钩子,传入 compiler 实例和回调,订阅该钩子的插件有:
- NodeEnvironmentPlugin:
js
class NodeEnvironmentPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem) {
compiler.fsStartTime = Date.now();
inputFileSystem.purge(); // 出清文件系统
}
});
}
}
如果输入文件系统是给定的inputFileSystem,则记录当前时间并清除该文件系统。
此前的文件系统中的内容没用了,这已经是一次新编译了,前面的文件输入不要了。
- ProgressPlugin:
js
class ProgressPlugin {
constructor () {}
apply (compiler) {
interceptHook(compiler.hooks.beforeRun, 0.01, "setup", "before run");
}
}
简化后的插件定义的比较简单,这个插件顾名思义,输出 webpack 构建进度的,订阅 beforeRun 的钩子,输出 0.01。你会发现这个进度是人为定义的😂。
TIPS: 如果想看着快,这里直接输出99%😂
3.2.2 compiler.hooks.run.call
在 compiler.hooks.beforeRun 的回调里触发 hooks.run 钩子,webpack 内部暂无插件订阅该钩子;
3.2.3 调用 compiler.readRecords 方法
js
class Compiler {
readRecords(callback) {
if (this.hooks.readRecords.isUsed()) {
if (this.recordsInputPath) {
asyncLib.parallel([
cb => this.hooks.readRecords.callAsync(cb),
this._readRecords.bind(this)
]);
} else {
this.records = {};
this.hooks.readRecords.callAsync(callback);
}
} else {
if (this.recordsInputPath) {
this._readRecords(callback);
} else {
this.records = {};
callback();
}
}
}
}
首先判断是否注册了 compiler.hooks.readRecords 钩子,如果有则判断是否有 recordsInputPath 配置,有则触发 this.hooks.readRecords,这样会触发使用 records 的相关插件执行。然后调用 this._readRecords 方法:
js
class Compielr {
//....
_readRecords(callback) {
if (!this.recordsInputPath) {
this.records = {};
return callback();
}
this.inputFileSystem.stat(this.recordsInputPath, err => {
this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
this.records = parseJson(content.toString("utf-8"));
return callback();
});
});
}
}
_readRecords 方法的实现也很简单,调用前面我们说过的 inputFileSystem.stat 判断给定的 recordsInputPath 是否存在,若存在则读取这个 records 文件,得到结果后解析成 json 对象并复制给 this.records 属性;
如果没有 recordsInputPath,则直接置 this.records 为对象,然后触发 this.hooks.readRecords 钩子;
3.2.4 调用 compiler.compile() 方法
在完成上面的 records 文件读取后,即 this.readRecords 的回调中,调用 this.compile() 方法并传入 onCompiled;
compile 方法封装了编译的流程,即包含了 compiler 的生命周期: compiler.hooks.beforeCompile 到 hooks.afterCompile 中间的所有过程;
这个方法我们后面展开讲,这里不做过多的展开。
3.2.5 onCompiled 函数
on 这个介词表示在一个具体的时刻,所以 compiled 后面肯定是标识的是编译结束后。所以这个命名就是处理编译结束后的工作的。
上面的 compiler.compile 方法处理的 beforeComile 到 afterCompile 钩子,onCompiled 函数内部则处理后续的编译产物和 records 的写入工作是否继续,并在得到肯定结果后进行写入工作。
-
参数:
- 1.1 err 对象,编译失败
- 1.2 compilation对象:
-
详细工作如下:
- 2.1 判断 err,若 err 非空则说明编译失败,调用 finalCallback 终止编译;
- 2.2 传入 compilation 触发 this.hooks.shouldEmit.call 钩子,这个 shouldEmit 标识是否输出编译产物,如果不想让产物输出,则可以订阅这个钩子,并在这个钩子最后返回 false,这样即可阻止产物写入本地文件系统(本地磁盘);
- 2.3 在下个事件循环开头开始执行文件写入工作,这些工作位 compiler.emitAssets 封装,这里不展开这个方法;
- 2.4 完成编译产物的写入工作后(this.emitAssets 方法的回调)调用 compiler.emitRecords 方法进行 records 文件的写入工作;
- 2.5 records 完成后调用 finalCallback 函数完成最终的收尾工作;
js
const onCompiled = (err, compilation) => {
if (err) return finalCallback(err);
if (this.hooks.shouldEmit.call(compilation) === false) {
// 阻止文件写入
compilation.startTime = startTime;
compilation.endTime = Date.now();
const stats = new Stats(compilation);
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
process.nextTick(() => {
// 写入文件
this.emitAssets(compilation, err => {
if (compilation.hooks.needAdditionalPass.call()) {
// 暂时忽略
}
this.emitRecords(err => {
compilation.startTime = startTime;
compilation.endTime = Date.now();
const stats = new Stats(compilation);
this.hooks.done.callAsync(stats, err => {
this.cache.storeBuildDependencies(
compilation.buildDependencies,
err => {
return finalCallback(null, stats);
}
);
});
});
});
});
};
3.2.6 finalCallback 函数
finalCallback 顾名思义,最后的回调了,这个用于处理 compiler.run 方法的收尾工作;
- 函数的参数:
- 1.1 err,错误对象,标识编译失败;
- 1.2 stats,stat 统计信息对象,编译的时候的错误、bundle 信息等都包含在内了;
- 具体工作如下:
- 2.1 将 compiler.idle 置为 true,前面说过这个 idle 是处理持久化缓存的标识,开始空闲起来了(我愿将 idle 翻译成 摸鱼 😂)
- 2.2 调用 compiler.cache.beginIdle() 方法,前面讲过 compiler.cache 对象负责调度缓存的读写工作,这里开始 compiler.cache.beginIdle 发出了
"编译器空闲,请启动缓存写入工作"
的指令,后面的工作交给 IdleFileCachePlugin 和 PackFileCacheStrategy 完成; - 2.3 compiler.running 置为 false,标识本编译器停止工作了(下班了,溜了溜了😂)
- 2.4 如果有 callback 则调用 callback,这个 callback 是前面 webpack-cli 里面启动编译器时传入的,这里算是 webpack 编译器和 webpack-cli 在通信了。
- 2.5 触发 compiler.hooks.afterDone 钩子,告知订阅插件做收编译工作总结,编译工作收官;
js
const finalCallback = (err, stats) => {
this.idle = true;
this.cache.beginIdle();
this.idle = true;
this.running = false;
if (err) {
this.hooks.failed.call(err);
}
if (callback !== undefined) callback(err, stats);
this.hooks.afterDone.call(stats);
};
四、总结
本篇小作文完成了启动编译流程的 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 回调函数;