webpack5的编译主流程
根据watch选项调用compiler.watch或者是compiler.run()方法
try {
const { compiler, watch, watchOptions } = create();
if (watch) {
compiler.watch(watchOptions, callback);
} else {
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(
err || err2,
/** @type {options extends WebpackOptions ? Stats : MultiStats} */
(stats)
);
});
});
}
return compiler;
} catch (err) {
process.nextTick(() => callback(/** @type {Error} */ (err)));
return null;
}
compiler.run方法
来自于Compiler.prototype.run方法webpack/lib/Compiler.js
// 代码精简过
class Compiler {
constructor () {}
run(callback) {
// 判断compiler.running状态,防止重复执行相同的编译任务,若处于执行状态,则抛出异常终止本次调用
if (this.running) {
return callback(new ConcurrentCompilationError());
}
// finalCallback是编译流程的最终回调,持久化缓存的写入信号就是在这里释放的。
const finalCallback = (err, stats) => {};
// 设置compiler.running方法为true
this.running = true;
// 声明onCompiled内部方法,用于处理编译过程中的事件回调,根据编译的状态和钩子函数的返回值执行不同的操作
const onCompiled = (err, compilation) => { };
// 声明内部的run方法。
const run = () => {};
// 判断当前的空闲状态。该标识符在持久化缓存写入的时候为true,根据状态不同有不同的处理,true则需要等待缓存处理结束的会调里调用run方法启动编译。this.idle为false,则直接调用run方法
if (this.idle) {
this.cache.endIdle(err => {
this.idle = false;
run();
});
} else {
run();
}
}
}
run方法
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);
});
});
});
};
compiler.hooks.beforeRun.call
触发compiler.hooks.beforeRun钩子,传入compiler实例和回调。订阅该钩子的插件有
- NodeEnvironmentPlugin
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,则记录当前的时间,重新编译,此前的文件系统中的内容没用了
2. ProgressPlugin
class ProgressPlugin {
// 简化后的
constructor () {
apply(compiler) {
interceptHook(compiler.hooks.beforeRun, 0.01, "setup", "before run")
}
}
}
输出webpack构建进度的
compiler.hooks.run.call
在compiler.hooks.beforeRun的回调中触发hooks.run钩子,webpack内部暂时无法插件订阅该钩子
compiler.readRecords方法
//代码简化
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.bind(this)
this._readRecords
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();
});
});
}
}
onCompiled函数
上面的函数处理的是beforeCompile到afterCompile钩子,onCompiled函数则处理后续的编译产物和records的写入工作。
const onCompiled = (err, compilation) => {
// err对象,编译失败,调用finalCallback这个函数终止编译
if (err) return finalCallback(err);
// 触发this.hooks.shouldEmit.call这个钩子。
// shouldEmit标识是否输出编译产物,如果不想让产物输出,可以订阅这个钩子,并在这个钩子最后返回false,这样可以防止产物写入本地文件系统。
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 => {
// records完成后调用finalCallback函数完成最终的收尾工作
return finalCallback(null, stats);
}
);
});
});
});
});
};
finalCallback函数
用于处理compiler.run方法的收尾工作
const finalCallback = (err, stats) => {
// idle是处理持久化缓存的标识
this.idle = true;
this.cache.beginIdle();
this.idle = true;
// 关闭本编译器
this.running = false;
if (err) {
this.hooks.failed.call(err);
}
// callback是webpack-cli里面启动编译器传入的,这里算是webpack编译器和webpack-cli在通信。
if (callback !== undefined) callback(err, stats);
// 触发compiler.hooks.afterDone钩子,告知订阅插件做收编译工作的总结。
this.hooks.afterDone.call(stats);
};
调用compiler.cache.benginIdle()方法, 标识编译器空闲,可以进行缓存写入工作。后续在IdleFileCachePlugin和PackFileCacheStrategy完成
webpack流程
webpack的运行是一个串行的过程
- 初始化: 启动构建,读取以及合并参数,加载plugin,实例化compiler
- 编译,从entry发出,针对module串行调用对应的loader去翻译文件内容,然后找到module依赖的module,递归的进行编译处理
- 输出: 对编译后的module组合称为chunk,把chunk转换为文件,输出到文件系统
未开启监听模式不会有文件发生变化的那个箭头,开启监听模式为
初始化阶段
合并shell和配置文件的参数形成实例化complier对象 => 加载插件 => 处理入口
- 初始化参数:从配置文件和shell语句中读取合并参数,得到最终的参数。还会执行配置文件中的插件实例化语句new plugin()
- 实例化compiler: 从上一步的参数初始化compiler实例,compiler实例负责整个文件的监听和启动编译,compiler中包含了完整的webpack配置。全局中只有一个compiler实例对象
- 加载插件: 依次调用插件的apply方法,让插件可以监听后续的所有事件节点,并且传入compiler实例的引用,方便插件可以通过compiler调用webpack提供的api
- enviroment:开始应用node.js风格的文件系统到compiler对象,以方便后续的文件寻找和读取
- entry-option:读取配置的entry是,为每个entry实例化一个对应的entryplugin,为后面的entry的递归解析做准备。
- after-plugin: 调用完所有的内置和配置的插件的apply方法
- after-resolvers: 根据配置初始化弯沉resolver,resolver负责在文件系统中寻找指定路径的文件
编译阶段
- before-run: 清除缓存
- run: 启动一次新的编译
- watch-run: 和run类型,实在监视模式下启动的编译,这个事件中可以获得到哪些文件发生了变化导致重新启动一次新的编译
- compile: 告诉插件一次新的编译要启动,会给插件带上compiler对象
- compilation: 当webpack以开发模式运行的时候,当检测到文件变化的时候,一次新的compilation将会被创建,一个compilation对象包含了当前的模块资源,编译生成资源,变化的文件等,compilation对象也提供了很多事件回调供插件做拓展
- make: 一个新的compilation创建完毕,就会从entry开始读取文件,根据文件类型和配置loader对文件进行编译,编译完成之后再找出对该文件依赖的文件,递归的进行编译和解析
- after-compile: 一次compilation执行完成,这里会根据编译结果,合并出我们最终生成的文件名和文件内容
- invalid: 当遇到文件不存在,文件编译错误等的异常情况下会触发这种事件,该事件不会导致webpack退出。
compilation实际上就是调用相应的loader处理文件生成chunks并对这些chunks做优化的过程
- build-module:使用对应的loader去转换一个模块
- normal-module-loader: 使用loader对一个模块进行转换完之后,使用acorn转换后面的内容,输出对应的抽象语法树,方便以后webpack对后面的代码进行分析
- program: 从配置的入口模块开始,分析其ast,当遇到require等导入其他模块的语句之后,将其加到依赖的模块列表中,同时找到新找出的依赖模块进行递归分析,最后搞清楚所有模块的依赖关系
- seal: 所有模块及其依赖通过loader进行转换完成之后,根据依赖开始生成chunk
输出阶段
- should-emit: 所有的输出文件已经生成好,询问插件那些文件需要输出,那些不需要
- emit: 确定好那些文件后,执行文件输出,可以再这里获取和修改输出内容
- after-emit: 文件输出完毕
- done: 成功完成一次完成编译和输出流程
- failed: 如果再编译和输出过程中遇到异常或者是导致webpack退出的时候,就会跳到此步骤,插件可以再本事件中获取到具体的错误原因。