在上一篇博客中分析了webpack打包以后的bundle文件,了解webpack是如何通过require
模拟commonjs标准加载模块的。下面探索webpack整体调用的流程,也就是如何通过shell输入webpack命令就可以实现整个编译、打包过程的。该系列博客的所有测试代码。
这篇博客只对整个流程及相关的事件流进行分析,不具体分析每个步骤中的具体实现。也就是,对于plugin和loader的具体分析请移步后面的博客。
整体调用流程
只有了解整体流程以后,才更容易找到你关心功能的源码。比如,如果你想知道plugin的工作原理,面对webpack的庞大源码,很难找到到底哪些js文件与plugin相关。于是我想先了解整体流程。
思路
捋顺流程的思路很简单,首先找到node_modules/.bin/webpack.cmd
查看webpack
命令干了什么:
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\..\webpack\bin\webpack.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\..\webpack\bin\webpack.js" %*
)
在安装node的情况下,会执行webpack/bin/wepack.js
,下面看看这个文件进行了哪些操作。
- 通过
[yargs](https://www.npmjs.com/package/yargs)
获得shell中的参数 - 把
webpack.config.js
中的参数和shell参数整合到options
对象上 - 调用
lib/webpack.js
开始编译和打包
依照这个思路继续查看lib/webpack.js
。不再一步一步仔细分析,下面直接展示分析后的结果。
流程
下图展示了webpack主要流程涉及的类及其调用关系。
lib/webpack.js
中返回一个compiler
对象,并调用了compiler.run()
lib/Compiler.js
中,run
方法触发了before-run
、run
两个事件,然后通过readRecords
读取文件,通过compile
进行打包,打包后触发before-compile
、compile
、make
等事件;compile
是主要流程,该方法中实例化了一个Compilation
类,并调用了其finish
及seal
方法,图中没有展开compile
方法具体代码lib/Compilation.js
中定义了finish
及seal
方法,还有一个重要方法addEntry
。这个方法通过调用其私有方法_addModuleChain
完成了两件事:根据模块的类型获取对应的模块工厂并创建模块;构建模块lib/Compiler.js
中没有显式调用addEntry
,而是触发make
事件,lib/DllEntryPlugin.js
为一个监听make
事件的插件,在回调函数中调用了addEntry
具体分析_addModuleChain
,其完成的第二件事构建模块又可以分为三部分:
- 调用
loader
处理模块之间的依赖 - 将
loader
处理后的文件通过[acorn](https://github.com/ternjs/acorn)
抽象成抽象语法树AST - 遍历AST,构建该模块的所有依赖
至此,我们知道了,plugin是通过事件监听的方式实现(DllEntryPlugin.js
就是一个插件);loader相关源码可以从_addModuleChain
为切入点看。
但是,我们多了几个疑惑:webpack到底有哪些事件(些plugin时候需要),这些事件流是如何控制的,每个事件都在什么阶段触发?
Tapable事件流
由第一部分分析可以知道,webpack整个流程是用事件进行控制的,也就是,不同的功能函数都封装到plugin中,plugin相当于listener,主打包流程中调用各种apply
方法(如:applyPluginsAsync
等)触发plugin
,apply
方法相当于emitter。该流程通过Tapable
类进行控制,上部分中 Compiler
和Compilation
两类都继承自Tapable
。那么,到底webpack中到底有哪些事件,不同的apply
方法具体行为有什么不一样呢?
webpack中的事件
webpack中的事件如下,这些事件出现的顺序固定,但不一定每次打包所有事件都触发:
类型 | 名字 | 事件名 |
---|---|---|
[C] | applyPluginsBailResult | entry-option |
[A] | applyPlugins | after-plugins |
[A] | applyPlugins | after-resolvers |
[A] | applyPlugins | environment |
[A] | applyPlugins | after-environment |
[D] | applyPluginsAsyncSeries | run |
[A] | applyPlugins | normal-module-factory |
[A] | applyPlugins | context-module-factory |
[A] | applyPlugins | compile |
[A] | applyPlugins | this-compilation |
[A] | applyPlugins | compilation |
[F] | applyPluginsParallel | make |
[E] | applyPluginsAsyncWaterfall | before-resolve |
[B] | applyPluginsWaterfall | factory |
[B] | applyPluginsWaterfall | resolver |
[A] | applyPlugins | resolve |
[A] | applyPlugins | resolve-step |
[G] | applyPluginsParallelBailResult | file |
[G] | applyPluginsParallelBailResult | directory |
[A] | applyPlugins | resolve-step |
[G] | applyPluginsParallelBailResult | result |
[E] | applyPluginsAsyncWaterfall | after-resolve |
[C] | applyPluginsBailResult | create-module |
[B] | applyPluginsWaterfall | module |
[A] | applyPlugins | build-module |
[A] | applyPlugins | normal-module-loader |
[C] | applyPluginsBailResult | program |
[C] | applyPluginsBailResult | statement |
[C] | applyPluginsBailResult | evaluate CallExpression |
[C] | applyPluginsBailResult | var data |
[C] | applyPluginsBailResult | evaluate Identifier |
[C] | applyPluginsBailResult | evaluate Identifier require |
[C] | applyPluginsBailResult | call require |
[C] | applyPluginsBailResult | evaluate Literal |
[C] | applyPluginsBailResult | call require:amd:array |
[C] | applyPluginsBailResult | evaluate Literal |
[C] | applyPluginsBailResult | call require:commonjs:item |
[C] | applyPluginsBailResult | statement |
[C] | applyPluginsBailResult | evaluate MemberExpression |
[C] | applyPluginsBailResult | evaluate Identifier console.log |
[C] | applyPluginsBailResult | call console.log |
[C] | applyPluginsBailResult | expression console.log |
[C] | applyPluginsBailResult | expression console |
[A] | applyPlugins | succeed-module |
[E] | applyPluginsAsyncWaterfall | before-resolve |
[B] | applyPluginsWaterfall | factory |
[A] | applyPlugins | build-module |
[A] | applyPlugins | succeed-module |
[A] | applyPlugins | seal |
[A] | applyPlugins | optimize |
[A] | applyPlugins | optimize-modules |
[A] | applyPlugins | after-optimize-modules |
[A] | applyPlugins | optimize-chunks |
[A] | applyPlugins | after-optimize-chunks |
[D] | applyPluginsAsyncSeries | optimize-tree |
[A] | applyPlugins | after-optimize-tree |
[C] | applyPluginsBailResult | should-record |
[A] | applyPlugins | revive-modules |
[A] | applyPlugins | optimize-module-order |
[A] | applyPlugins | before-module-ids |
[A] | applyPlugins | optimize-module-ids |
[A] | applyPlugins | after-optimize-module-ids |
[A] | applyPlugins | record-modules |
[A] | applyPlugins | revive-chunks |
[A] | applyPlugins | optimize-chunk-order |
[A] | applyPlugins | before-chunk-ids |
[A] | applyPlugins | optimize-chunk-ids |
[A] | applyPlugins | after-optimize-chunk-ids |
[A] | applyPlugins | record-chunks |
[A] | applyPlugins | before-hash |
[A] | applyPlugins | hash |
[A] | applyPlugins | hash-for-chunk |
[A] | applyPlugins | chunk-hash |
[A] | applyPlugins | after-hash |
[A] | applyPlugins | before-chunk-assets |
[B] | applyPluginsWaterfall | global-hash-paths |
[C] | applyPluginsBailResult | global-hash |
[B] | applyPluginsWaterfall | bootstrap |
[B] | applyPluginsWaterfall | local-vars |
[B] | applyPluginsWaterfall | require |
[B] | applyPluginsWaterfall | module-obj |
[B] | applyPluginsWaterfall | module-require |
[B] | applyPluginsWaterfall | require-extensions |
[B] | applyPluginsWaterfall | asset-path |
[B] | applyPluginsWaterfall | startup |
[B] | applyPluginsWaterfall | module-require |
[B] | applyPluginsWaterfall | render |
[B] | applyPluginsWaterfall | module |
[B] | applyPluginsWaterfall | render |
[B] | applyPluginsWaterfall | package |
[B] | applyPluginsWaterfall | module |
[B] | applyPluginsWaterfall | render |
[B] | applyPluginsWaterfall | package |
[B] | applyPluginsWaterfall | modules |
[B] | applyPluginsWaterfall | render-with-entry |
[B] | applyPluginsWaterfall | asset-path |
[B] | applyPluginsWaterfall | asset-path |
[A] | applyPlugins | chunk-asset |
[A] | applyPlugins | additional-chunk-assets |
[A] | applyPlugins | record |
[D] | applyPluginsAsyncSeries | additional-assets |
[D] | applyPluginsAsyncSeries | optimize-chunk-assets |
[A] | applyPlugins | after-optimize-chunk-assets |
[D] | applyPluginsAsyncSeries | optimize-assets |
[A] | applyPlugins | after-optimize-assets |
[D] | applyPluginsAsyncSeries | after-compile |
[C] | applyPluginsBailResult | should-emit |
[D] | applyPluginsAsyncSeries | emit |
[B] | applyPluginsWaterfall | asset-path |
[D] | applyPluginsAsyncSeries | after-emit |
[A] | applyPlugins | done |
列举几个关键的事件对应打包的阶段:
-
entry-option
:初始化options -
run
:开始编译 -
make
:从entry开始递归分析依赖并对依赖进行build -
build-moodule
:使用loader加载文件并build模块 -
normal-module-loader
:对loader加载的文件用acorn编译,生成抽象语法树AST -
program
:开始对AST进行遍历,当遇到require
时触发call require
事件 -
seal
:所有依赖build完成,开始对chunk进行优化(抽取公共模块、加hash等) -
optimize-chunk-assets
:压缩代码 -
emit
:把各个chunk输出到结果文件
如果你想写一个plugin,需要先了解这些事件,根据plugin的功能选择其监听的事件。
Tapable类中事件监听者的执行顺序
在经典的观察者模式中,触发事件是很简单的事,为什么webpack中专门有一个类Tapable完成这个工作呢?
为了回答这个问题,先看看webpack中事件的监听者也就是plugin的结构:
opitions = {
......
"plugins":{
//每个事件多个监听者,这些监听者放在一个数组中
"before-run": [],
"run": [],
...
}
......
}
当触发一个事件的时候,就需要确定该事件对应的监听者执行的顺序,
监听者出错后如何纠错。所以,需要一个类来统一处理这些问题。
下面列举几个Tapable中控制监听者执行顺序的apply
方法。
applyPluginsAsync
模拟几个plugin,监听一个自定义的something
事件,然后用applyPluginsAsync
方法触发该事件:
var Tapable = require('./Tapable.js');
var tapable = new Tapable();
tapable._plugins = {
"something": [
function(a, cb){
setTimeout(()=>{
console.log('1', a);
cb();
},1500);
},
function(a, cb){
setTimeout(()=>{
console.log('2', a);
cb();
},1000);
},
function(a, cb){
setTimeout(()=>{
console.log('3', a);
cb();
},500);
}
]
}
// applyPluginsAsync
tapable.applyPluginsAsync('something', 'applyPluginsAsync', function(){console.log('end');});
打印结果:
结果为强制顺序执行plugin,第一个执行完才执行第二个。如果出现错误:
tapable._plugins = {
"something": [
function(a, cb){
setTimeout(()=>{
console.log('1', a);
cb();
},1500);
},
function(a, cb){
setTimeout(()=>{
console.log('2', a);
//出现错误
cb(new Error('error message'));
},1000);
},
function(a, cb){
setTimeout(()=>{
console.log('3', a);
cb();
},500);
}
]
}
// applyPluginsAsync
tapable.applyPluginsAsync('something', 'applyPluginsAsync', function(){console.log('end');});
控制台打印结果:
第二个plugin出现错误,则直接调用最后的回调函数,第三个plugin不再执行。
applyPluginsParallel
var Tapable = require('./Tapable.js');
var tapable = new Tapable();
tapable._plugins = {
"something": [
function(a, cb){
setTimeout(()=>{
console.log('1', a);
cb();
},1500);
},
function(a, cb){
setTimeout(()=>{
console.log('2', a);
//出现错误
cb(new Error('error message'));
},1000);
},
function(a, cb){
setTimeout(()=>{
console.log('3', a);
cb();
},500);
}
]
}
// applyPluginsParallel
tapable. applyPluginsParallel('something', ' applyPluginsParallel', function(){console.log('end');});
控制台打印结果:
同时开始执行所有plugin,当遇到错误时调用最后的回调函数,然后继续执行下面的plugin,后面plugin再出现错误不再调用回调函数,也就是说无论如何所有的plugin都会执行一次,当出现错误时回调函数会执行且仅执行一次。
applyPluginsWaterfall
var Tapable = require('./Tapable.js');
var tapable = new Tapable();
tapable._plugins = {
"anything": [
function(a, cb){
console.log('1', a);
let b = a+1;
cb();
return b;
},
function(a, cb){
console.log('2', a);
let b = a+1;
cb();
return b;
},
function(a, cb){
console.log('3', a);
let b = a+1;
cb();
return b;
}
]
}
//applyPluginsWaterfall
tapable. applyPluginsWaterfall('anything', 0, function(){console.log('end');});
控制台打印结果:
用这种方法你可以测试Tapable中的其他方法,也可以查看文档了解这些apply方法。
另外,你在Tapable中所有的apply方法中插入console
,然后进行一次打包就可以看到以上事件的触发顺序啦。
总结
总结了webpack从输入命令到打包完成的大致整体流程后,我们找到了plugin的调用方式及loader相关源码的位置。完成这些工作,我们对webpack有了大体了解,下面就可以逐一分析plugin、loader、code-spliting等特性在源码中是如何完成的。