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

上一篇博客中分析了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-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]applyPluginsBailResultentry-option
[A]applyPluginsafter-plugins
[A]applyPluginsafter-resolvers
[A]applyPluginsenvironment
[A]applyPluginsafter-environment
[D]applyPluginsAsyncSeriesrun
[A]applyPluginsnormal-module-factory
[A]applyPluginscontext-module-factory
[A]applyPluginscompile
[A]applyPluginsthis-compilation
[A]applyPluginscompilation
[F]applyPluginsParallelmake
[E]applyPluginsAsyncWaterfallbefore-resolve
[B]applyPluginsWaterfallfactory
[B]applyPluginsWaterfallresolver
[A]applyPluginsresolve
[A]applyPluginsresolve-step
[G]applyPluginsParallelBailResultfile
[G]applyPluginsParallelBailResultdirectory
[A]applyPluginsresolve-step
[G]applyPluginsParallelBailResultresult
[E]applyPluginsAsyncWaterfallafter-resolve
[C]applyPluginsBailResultcreate-module
[B]applyPluginsWaterfallmodule
[A]applyPluginsbuild-module
[A]applyPluginsnormal-module-loader
[C]applyPluginsBailResultprogram
[C]applyPluginsBailResultstatement
[C]applyPluginsBailResultevaluate CallExpression
[C]applyPluginsBailResultvar data
[C]applyPluginsBailResultevaluate Identifier
[C]applyPluginsBailResultevaluate Identifier require
[C]applyPluginsBailResultcall require
[C]applyPluginsBailResultevaluate Literal
[C]applyPluginsBailResultcall require:amd:array
[C]applyPluginsBailResultevaluate Literal
[C]applyPluginsBailResultcall require:commonjs:item
[C]applyPluginsBailResultstatement
[C]applyPluginsBailResultevaluate MemberExpression
[C]applyPluginsBailResultevaluate Identifier console.log
[C]applyPluginsBailResultcall console.log
[C]applyPluginsBailResultexpression console.log
[C]applyPluginsBailResultexpression console
[A]applyPluginssucceed-module
[E]applyPluginsAsyncWaterfallbefore-resolve
[B]applyPluginsWaterfallfactory
[A]applyPluginsbuild-module
[A]applyPluginssucceed-module
[A]applyPluginsseal
[A]applyPluginsoptimize
[A]applyPluginsoptimize-modules
[A]applyPluginsafter-optimize-modules
[A]applyPluginsoptimize-chunks
[A]applyPluginsafter-optimize-chunks
[D]applyPluginsAsyncSeriesoptimize-tree
[A]applyPluginsafter-optimize-tree
[C]applyPluginsBailResultshould-record
[A]applyPluginsrevive-modules
[A]applyPluginsoptimize-module-order
[A]applyPluginsbefore-module-ids
[A]applyPluginsoptimize-module-ids
[A]applyPluginsafter-optimize-module-ids
[A]applyPluginsrecord-modules
[A]applyPluginsrevive-chunks
[A]applyPluginsoptimize-chunk-order
[A]applyPluginsbefore-chunk-ids
[A]applyPluginsoptimize-chunk-ids
[A]applyPluginsafter-optimize-chunk-ids
[A]applyPluginsrecord-chunks
[A]applyPluginsbefore-hash
[A]applyPluginshash
[A]applyPluginshash-for-chunk
[A]applyPluginschunk-hash
[A]applyPluginsafter-hash
[A]applyPluginsbefore-chunk-assets
[B]applyPluginsWaterfallglobal-hash-paths
[C]applyPluginsBailResultglobal-hash
[B]applyPluginsWaterfallbootstrap
[B]applyPluginsWaterfalllocal-vars
[B]applyPluginsWaterfallrequire
[B]applyPluginsWaterfallmodule-obj
[B]applyPluginsWaterfallmodule-require
[B]applyPluginsWaterfallrequire-extensions
[B]applyPluginsWaterfallasset-path
[B]applyPluginsWaterfallstartup
[B]applyPluginsWaterfallmodule-require
[B]applyPluginsWaterfallrender
[B]applyPluginsWaterfallmodule
[B]applyPluginsWaterfallrender
[B]applyPluginsWaterfallpackage
[B]applyPluginsWaterfallmodule
[B]applyPluginsWaterfallrender
[B]applyPluginsWaterfallpackage
[B]applyPluginsWaterfallmodules
[B]applyPluginsWaterfallrender-with-entry
[B]applyPluginsWaterfallasset-path
[B]applyPluginsWaterfallasset-path
[A]applyPluginschunk-asset
[A]applyPluginsadditional-chunk-assets
[A]applyPluginsrecord
[D]applyPluginsAsyncSeriesadditional-assets
[D]applyPluginsAsyncSeriesoptimize-chunk-assets
[A]applyPluginsafter-optimize-chunk-assets
[D]applyPluginsAsyncSeriesoptimize-assets
[A]applyPluginsafter-optimize-assets
[D]applyPluginsAsyncSeriesafter-compile
[C]applyPluginsBailResultshould-emit
[D]applyPluginsAsyncSeriesemit
[B]applyPluginsWaterfallasset-path
[D]applyPluginsAsyncSeriesafter-emit
[A]applyPluginsdone

列举几个关键的事件对应打包的阶段:

  • 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等特性在源码中是如何完成的。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页