webpack打包流程
2. webpack事件流 && 钩子
webpack事件流可以认为是webpack在构建过程中会在对应的编译阶段触发的一系列事件,而这些事件通过webpack广播后,plugin插件监听到对应事件名,拿到当前阶段的文件资源,通过回调函数执行特殊功能处理。在整个webapck编译的事件流中都是通过Tapable进行管理。
2.0 tabpable原理
Tapable的原理其实就是我们在前端进阶过程中都会经历的EventEmit,通过发布者-订阅者模式实现,它的部分核心代码可以概括成下面这样:
class SyncHook{
constructor(){
this.hooks = [];
}
// 订阅事件
tap(name, fn){
this.hooks.push(fn);
}
// 发布
call(){
this.hooks.forEach(hook => hook(...arguments));
}
}
webpack事件流中compiler主要的钩子,如下图 :
ps.这里借用一下juejin.cn/post/692049… 大佬的图哈
webpack事件流主要分为四个阶段:
- 初始化阶段:创建编译器
- 编译阶段:编译器运行
- 输出阶段:编译器输出
2.1 初始化阶段:创建编译器
compiler = createCompiler(options); // 创建compiler
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
// 注意:执行完后触发三个钩子-API:environment、afterEnvironment、initialize
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
初始化阶段:
(1)初始化options配置:webpack会读取你在的shell命令行传入的配置和项目中的webpack.config.js文件中的配置(如果配置文件中存在plugin实例化语句也会一并执行:new plugin().),初始化本次构建所需要的配置参数;
(2)生成compiler实例:得到上一阶段完整的配置化参数后,生成compiler实例(上一阶段的实例也会被放置在compiler实例中)
(3)依次执行plugin的apply:生成compiler实例后,就开始为webpack事件流上挂载对应的plugin钩子,即执行各个插件实例的apply方法(注意:apply方法中传入compiler实例,并且在钩子回调中传入compileration实例用来拿到此阶段编译资源)。
(4)为complier配置node环境方便读取文件
(5)entry-options:即开始读取配置中的entry参数,递归遍历所有入口
2.2 编译阶段:编译器运行编译阶段
const { compiler, watch, watchOptions } = create(); // 1.创建编译器阶段
if (watch) {
compiler.watch(watchOptions, callback);
} else {
compiler.run((err, stats) => { // 2.如果没有开启watch,则运行编译器对象的逻辑
compiler.close(err2 => {
callback(err || err2, stats);
});
});
}
进入compiler编译器实例的编辑运行阶段,执行run函数。注意只是进入到compiler实例的运行阶段,不是编译阶段,编译阶段是中编译开始的标志是make函数的执行。而这之间还有好多函数要处理。
run函数逻辑
1.触发beforeRun钩子
2.触发run钩子
3.执行compile函数
const run = () => {
this.hooks.beforeRun.callAsync(this, err => { // 1.执行beforeRun钩子
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => { // 2.执行run钩子
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled); // 3.执行compile()编译函数
});
});
});
};
compile()编译函数
1.先执行beforeCompile的async钩子,在这个钩子的回调函数中执行make钩子。
2.然后再执行make函数后,此时进入到真正的编译流程(从entry入口出发,根据文件类型和配置loader进行编译)
3.编译完成后执行finishMake钩子(传入compileration对象)
4.compilation.finish
5.compilation.seal(生成chunk)
6.hooks.afterCompile钩子
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
const compilation = this.newCompilation(params); //beforecompiler函数中新建了compileration类,compileration专门用来编译源码,创建模块,compiler用来控制流程
this.hooks.make.callAsync(compilation, err => { // 执行make开始正式启动编译,当make执行完之后就表示编译结束了,可以对文件进行seal封装了
...
this.hooks.finishMake.callAsync(compilation, err => {
...
process.nextTick(() => {
...
compilation.finish(err => {
...
compilation.seal(err => {
...
this.hooks.afterCompile.callAsync(compilation, err => {
logger.timeEnd("afterCompile hook");
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
});
});
}
Compiler控制流程,Compilation专业解析,ModuleFactory生成模块,Parser解析源码,最后通过Template组合模块,输出打包文件的过程
编辑阶段:
(1)执行run方法
(2)执行compile方法开始编译:触发五个钩子beforeCompile
,compile
,make
,finishMake
,afterCompile
,当执行到make钩子之后就是开始真正的编译了。
(3)使用loader对文件进行编译,再将编译好的内容通过acron生成静态AST树,递归分析文件依赖并拉取。最后将所有模块中的require语法替换成_webpack_require_来实现模块化操作。
Compiler.run()函数 主要钩子:
- compiler.hooks.beforeRun;
- comiler.hooks.run: 这个钩子主要是处理缓存的模块,通过缓存的模块就可以减少需要编译的模块,进而加快编译的速度,之后进入this.compile()函数
Compiler.compile()函数 主要钩子
- beforeCompile
- compile
- make(真正的编译,非常重要)
- finishMake
- afterCompile
总结:
梳理一下编译阶段compiler对象的函数执行栈: compiler.run-> compiler.compile -> compiler.onCompiled
- run函数中触发的钩子:beforeRun,run
- compile函数中触发的钩子:beforeCompile,compile,thisCompilation,compilation,make,afterCompile
- onCompiled函数中触发的钩子: should-emit,emit,done。
1.讲一下make钩子的过程:
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
});
)
1.预处理:先根据是单入口还是多入口,如果是单入口的话调用SingleEntryPlugin钩子,多入口调用mulpileEntryPlugin钩子。通过这类钩子的主要作用要是与模块类形成一个key-value对,如果是单入口的话就就使用单入口对应的模块类创建模块,如果是多入口的话就用多入口模块类创建模块,如果是动态加载就用动态模块类创建模块实例。将这个key-value存储到dep中。
2.执行addEntry函数:
2.1 调用_addModuleChai函数(目的:调用loader,构建模块,寻找依赖,最终形成模块链)
通过dep中对应的模块类构建模块对象,然后调用对象实例的build方法:调用loader,构建模块,寻找依赖。
递归寻找解析,直到所有模块构建完毕,形成模块链。
至此,make编译结束,make钩子回调中调用了compilation的seal方法。开始执行seal钩子,进行chunk构建和打包优化。
注意:这里loader调用时通过dobuild方法来执行runloders去使用loader解析文件内容为js内容(css。vue生成标准的js模块),laoder解析完之后会使用parse生成ast语法树
-
第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码
-
第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树
-
最后调用 compilation.seal 进入 render 阶段,根据之前收集的依赖,决定生成多少文件,每个文件的内容是什么
const { runLoaders } = require("loader-runner");
doBuild(options, compilation, resolver, fs, callback){
// runLoaders从包'loader-runner'引入的方法
runLoaders({
resource: this.resource, // 这里的resource可能是js文件,可能是css文件,可能是img文件
loaders: this.loaders,
}, (err, result) => {
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
const extraInfo = result.length >= 2 ? result[2] : null; // ...
})
}
2.seal钩子的过程:chunk构建和打包优化
- 调用seal钩子,seal方法中钩子可谓大展威力,seal方法只是调用各种钩子,真正的构建和优化工作,都是插件做的。
- 循环调用optimizeDependenciesBasic、optimizeDependencies、optimizeDependenciesAdvanced钩子。
- 调用afterOptimizeDependencies钩子。
- 调用beforeChunks钩子。从名字也可以看出上面做的依赖方面的优化,此处开始构建Chunk了。
- 调用afterChunks钩子。
- 调用optimize钩子。
- 调用optimizeModulesBasic、optimizeModules、optimizeModulesAdvanced、afterOptimizeModules钩子。此处优化module。,参数是this.modules。
- 调用optimizeChunksBasic、optimizeChunks、optimizeChunksAdvanced、afterOptimizeChunks。此处优化Chunk,参数是this.chunkGroups。
- 一系列钩子调用不再赘述,有需要的时候,我们再研究。
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
总之seal方法完成了Chunk的构建和依赖、Chunk、module等各方面的优化。 (juejin.cn/post/684490…
回到compiler
seal方法执行完毕,生成好了Chunks对象,compilation的工作告一段落,控制权又还给compiler,此时compiler的compile方法就执行完毕了。该执行compile的回调函数onCompiled了。上面我们也贴出来onCompiled的简要代码
onCompiled方法做的事
onCompiled完成了最后的输出阶段。将我们生成的Chunk输出到磁盘上。调用了done钩子之后,一次构建就此完成。
总结、compilation对象的职责
总结:构建模块和Chunk,并利用插件优化构建过程
2.3 输出阶段: 编译器输出阶段
输出阶段:
(1)所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets
上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。
shouldEmit、done
emit、afterEmit