Webpack-源码二,整体调用流程与Tapable事件流

时间:2021-02-11 19:59:56

上一篇博客中分析了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主要流程涉及的类及其调用关系。
Webpack-源码二,整体调用流程与Tapable事件流

  • lib/webpack.js中返回一个compiler对象,并调用了compiler.run()

  • lib/Compiler.js中,run方法触发了before-runrun两个事件,然后通过readRecords读取文件,通过compile进行打包,打包后触发before-compilecompilemake等事件;compile是主要流程,该方法中实例化了一个Compilation类,并调用了其finishseal方法,图中没有展开compile方法具体代码

  • lib/Compilation.js中定义了finishseal方法,还有一个重要方法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等)触发pluginapply方法相当于emitter。该流程通过Tapable类进行控制,上部分中 CompilerCompilation两类都继承自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');});

打印结果:
Webpack-源码二,整体调用流程与Tapable事件流

结果为强制顺序执行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');});

控制台打印结果:
Webpack-源码二,整体调用流程与Tapable事件流

第二个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');});

控制台打印结果:
Webpack-源码二,整体调用流程与Tapable事件流
同时开始执行所有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');});

控制台打印结果:
Webpack-源码二,整体调用流程与Tapable事件流

用这种方法你可以测试Tapable中的其他方法,也可以查看文档了解这些apply方法。
另外,你在Tapable中所有的apply方法中插入console,然后进行一次打包就可以看到以上事件的触发顺序啦。

总结

总结了webpack从输入命令到打包完成的大致整体流程后,我们找到了plugin的调用方式及loader相关源码的位置。完成这些工作,我们对webpack有了大体了解,下面就可以逐一分析plugin、loader、code-spliting等特性在源码中是如何完成的。