webpack编译过程中的重要“桥梁”-tapable

108 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

1.webpack中的Compiler

Compiler依赖的npm包,如下:

webpack-complier.png

 const isSorted = array => {}
 const sortObject = (obj, keys) => {}
 class Compiler {}
 module.exports = Compiler;

这三个部分就是Complier的主干。Compiler的机构总体如下:

image-20220814175217659.png

svg矢量图下载

2.Compiler构造函数中用到的tapable的不同类型的钩子

从constructor讲起,在Complier的构造函数中首先看到的是Object.freeze,freeze直译过来就是“冻结、冻僵”的意思。这里的Object.freeze方法顾名思义是冻结一个对象。冻结后不能被修改。如下图:

image-20220814174403315.png

在构造函数中初始化对象的属性对应如下几种类型:

  • SyncHook
  • SyncBailHook
  • AsyncSeriesHook
  • SyncHook
  • AsyncParallelHook

这几个方法都是tapable的内容,所以如果深入webpack的Compiler过程就需要跨过tapable的门槛。

3.tapable

在npm中tapable如下:

image-20220816082429855.png

(图片来自:www.npmjs.com/package/tap…

4.tapable实例及SyncHook的实现过程

结合官方给出的示例,改造部分内容如下:

 const path=require('path');
 ​
 console.log('-----learn tapable.start--------')
 ​
 const {
     SyncHook
  } = require("tapable");
 ​
  class Car {
     constructor() {
         this.hooks = {
             accelerate: new SyncHook(["newSpeed"],'askcomputer'),
             brake: new SyncHook()
         };
     }
     setSpeed(newSpeed) {
         this.hooks.accelerate.call(newSpeed);
     }
     setWarningLampOn(){
         this.hooks.brake.call();
     }
 }
 ​
 const myCar = new Car();
 const warningLamp={
      on:function(){
         console.log('------警示灯开启--------');
     }
 }
 myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
 myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
 ​
 myCar.setSpeed(20);
 myCar.setWarningLampOn();
 ​
 console.log('-----learn tapable.end--------')
 ​
 ​
 module.exports={
     entry:'./main.js',
     output:{
         filename:'bundle.js',
         path:path.resolve(__dirname,'../dist')
     },
     module:{
         rules:[
             {
                 test:/.css$/,
                 use:['style-loader','css-loader']
             }
         ]
     }
 }

(说明:因为主线是webpack,这里只是对tapable的内容介绍,用于辅助webpack的学习,所以这里把示例代码融合进了 webpack.config.js 使得可以在vscode下基于node环境调试。)

上述代码运行调试环境后输出如下:

image-20220816083617648.png

在官方的说明把它解释为钩子,实际上在笔者看来,它就是我们常见的基于观察者模式实现的事件监听机制。这么解释的话可能会更容易让人理解。

对于tapable中的实现,关键是两个地方,如上述代码中的tapcall

单步调试进入tapable 下的 Hook.js 中代码如下:

     _tap(type, options, fn) {
         ...
         options = this._runRegisterInterceptors(options);
         this._insert(options);
     }
     tap(options, fn) {
         this._tap("sync", options, fn);
     }

通过insert方法,返回得到this.taps的数组,得到的结果如下:

image-20220816092220202.png

紧接着查看,通过call调用的过程,代码中的方式是通过 myCar.setWarningLampOn 调用 CALL_DELEGATE 并且传递给 _createCall

 //Hook.js
 _createCall(type) {
         return this.compile({
             taps: this.taps,
             interceptors: this.interceptors,
             args: this._args,
             type: type
         });
     }

_createCall中就再次用到了构造对象的过程中,初始化时生成的taps。

关键是要看这个过程中的 taps 是如何调用的, 这个比较关键。

image-20220816095614422.png

 //SyncHook.js
 const COMPILE = function(options) {
     factory.setup(this, options);
     return factory.create(options);
 };
 ​
 //HookCodeFactory.js
 setup(instance,options)
 {
     //options.taps是在初始化构造过程中添加的tap方法数组,下面这一行通过数组的map方法过滤返回对应的fn
     instance._x=options.taps.map(t=>t.fn)
 }

image-20220816105939711.png

所以这里得到的instance._x就是taps中对应的fn

setup安装完成后,下一步:factory.create(options)

 //HookCodeFactory.js
 class HookCodeFactory {
         ...
         create(options) {
             this.init(options);
             let fn;
             switch (this.options.type) {
                 case "sync":
                     fn = new Function(
                         this.args(),
                         '"use strict";\n' +
                             this.header() +
                             this.contentWithInterceptors({
                                 onError: err => `throw ${err};\n`,
                                 onResult: result => `return ${result};\n`,
                                 resultReturns: true,
                                 onDone: () => "",
                                 rethrowIfPossible: true
                             })
                     );
                     break;
                     ...
 }

通过使用Function构造一个新的函数。第一个参数是函数的参数,第二个参数是函数的方法体。其中header()方法返回的内容如下:

image-20220816112640496.png

 var _context;var _x = this._x;

然后通过 contentWithInterceptors->content->callTapSeries->callTap,得到如下内容:

image-20220816113851597.png

对应的匿名函数格式化后,如下:

 (function anonymous() {
     "use strict";
     var _context;
     var _x = this._x; //这里的this._x就与上文中的 instance._x=options.taps.map(t=>t.fn) 相对应
     var _fn0 = _x[0];
     _fn0();
 })

到这就完成了一次this.hooks.brake.call();对应的函数调用。tapable中的 synchook 的整个环节就是这样一个流程。它相当于实现了一次事件监听机制。中间使用 Hook 这个钩子做为观察者进行观察。

本篇通过对tapable通过SyncHook的应用,说明了它从 tap 到 call 之间发生的过程。以此为基础,下一篇继续回归webpack的主线,继续webpack编译过程的原理说明。