Webpack插件機制之Tapable-源碼解析

Webpack的成功之處,不只在於強大的打包構建能力,也在於它靈活的插件機制。javascript

Webpack本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是Tapable。java

在學習Webpack的時候,常常能夠看到上述介紹。也就是說學Webpack的前提是要學習Tapable。才能更好的學習Webpack原理。node

1、Tapable

其實tapable的核心思路有點相似於node.js中的events,最基本的發佈/訂閱模式。webpack

const EventEmitter = require('events');
const myEmitter = new EventEmitter();

// 註冊事件對應的監聽函數
myEmitter.on('start', (params) => {
    console.log("輸出", params)
});

// 觸發事件 並傳入參數
myEmitter.emit('start', '學習webpack工做流'); // 輸出 學習webpack工做流
複製代碼

2、tapable鉤子介紹

首先,tapable提供的鉤子有以下10個。 web

tapable鉤子介紹

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesLoopHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");
複製代碼

其次,全部鉤子的用法簡介,以下:(能夠簡單瞄一眼,就往下看吧)數組

序號 鉤子名稱 執行方式 使用要點
1 SyncHook 同步串行 不關心監聽函數的返回值
2 SyncBailHook 同步串行 只要監聽函數中有一個函數的返回值不爲 undefined,則跳過剩下全部的邏輯
3 SyncWaterfallHook 同步串行 上一個監聽函數的返回值能夠傳給下一個監聽函數
4 SyncLoopHook 同步循環 當監聽函數被觸發的時候,若是該監聽函數返回true時則這個監聽函數會反覆執行,若是返回 undefined 則表示退出循環
5 AsyncParallelHook 異步併發 不關心監聽函數的返回值
6 AsyncParallelBailHook 異步併發 只要監聽函數的返回值不爲 null,就會忽略後面的監聽函數執行,直接跳躍到callAsync等觸發函數綁定的回調函數,而後執行這個被綁定的回調函數
7 AsyncSeriesHook 異步串行 不關心callback()的參數
8 AsyncSeriesBailHook 異步串行 callback()的參數不爲null,就會直接執行callAsync等觸發函數綁定的回調函數
9 AsyncSeriesWaterfallHook 異步串行 上一個監聽函數的中的callback(err, data)的第二個參數,能夠做爲下一個監聽函數的參數。
10 AsyncSeriesLoopHook 異步串行 能夠觸發handler循環調用。

3、上述Hook使用介紹

(1.1)SyncHook

同步串行,不關心監聽函數的返回值。bash

咱們先來介紹最簡單的SyncHook,其實每一個Hook都大同小異,懂一個其餘的就很是好懂了。併發

const {SyncHook} = require("tapable");

//全部的構造函數都接收一個可選的參數,這個參數是一個字符串的數組。
let queue = new SyncHook(['param1']); 

// 訂閱tap 的第一個參數是用來標識訂閱的函數的
queue.tap('event 1', function (param1) {
    console.log(param1, 1);
});

queue.tap('event 2', function (param1) {
    console.log(param1, 2);
});

queue.tap('event 3', function () {
    console.log(3);
});

// 發佈的時候觸發訂閱的函數 同時傳入參數
queue.call('hello');

// 控制檯輸出
/* hello 1 hello 2 3 */
複製代碼

能夠看到,這個鉤子訂閱的事件都是按順序同步執行的。app

(1.2)SyncHook原理

簡單模擬下原理。異步

class SyncHook{
    constructor(){
        this.taps = [];
    }

    // 訂閱
    tap(name, fn){
        this.taps.push(fn);
    }

    // 發佈
    call(){
        this.taps.forEach(tap => tap(...arguments));
    }
}
複製代碼

(2.1)SyncBailHook

再來看下SyncBailHook的使用。

只要監聽函數中有一個函數的返回值不爲undefined,則跳過剩下全部的邏輯。

let queue = new SyncBailHook(['param1']); //全部的構造函數都接收一個可選的參數,這個參數是一個字符串的數組。

// 訂閱
queue.tap('event 1', function (param1) {// tap 的第一個參數是用來標識訂閱的函數的
    console.log(param1, 1);
    return 1;
});

queue.tap('event 2', function (param1) {
    console.log(param1, 2);
});

queue.tap('event 3', function () {
    console.log(3);
});

// 發佈
queue.call('hello', 'world');// 發佈的時候觸發訂閱的函數 同時傳入參數

// 控制檯輸出
/* hello 1 */
複製代碼

能夠看到,只要監聽函數中有一個函數的返回值不爲undefined,則跳過剩下全部的邏輯。

(2.2)SyncBailHook原理

簡單模擬下原理。

class SyncBailHook {
    constructor() {
        this.taps = [];
    }

    // 訂閱
    tap(name, fn) {
        this.taps.push(fn);
    }

    // 發佈
    call() {
        for (let i = 0, l = this.taps.length; i < l; i++) {
            let tap = this.taps[i];
            let result = tap(...arguments);
            if (result) {
                break;
            }
        }
    }
}
複製代碼

(3)SyncHook和SyncBailHook總結

上述2種的鉤子的執行流程以下圖所示:

經過這個 2個鉤子的介紹,能夠發現 tapable提供了各類各樣的 hook來幫咱們管理事件是如何執行的。

tapable的核心功能就是控制一系列註冊事件之間的執行流控制,好比我註冊了三個事件,我能夠但願他們是併發的,或者是同步依次執行,又或者其中一個出錯後,後面的事件就不執行了,這些功能均可以經過tapablehook實現。

就像起牀、上班、吃早飯的關係同樣,起牀確定是優先的。可是吃飯和上班就不必定啦。萬一要遲到了呢?可能就放棄早飯了!

吃飯

4、Tapable的源碼解讀

記住重點,核心就是calltap兩個方法。
記住重點,核心就是calltap兩個方法。
記住重點,核心就是calltap兩個方法。

那咱們來看下tapable源碼的SyncHook是如何實現的,以下。仍是那句話,看完一個,其餘的天然就懂啦。爲了理解,源碼均爲縮減過的,去除了些非核心代碼。

// node_modules/tapable/lib/SyncHook.js
const factory = new SyncHookCodeFactory();
// 繼承基礎Hook類
class SyncHook extends Hook {
    // 重寫Hook的compile方法
    compile(options) {
        // 開發者訂閱的事件傳
        factory.setup(this, options);
        // 動態生成call方法
    	return factory.create(options);
    }
}
module.exports = SyncHook;
複製代碼

核心代碼很是簡單,能夠看到SyncHook就是繼承了Hook基礎類。並重寫了compile方法。

首先來看下Hook基礎類的tap方法。能夠看到每次調用tap,就是收集當前hook實例全部訂閱的事件到taps數組。

// node_modules/tapable/lib/Hook.js
// 訂閱
tap(options, fn) {
    // 同步 整理配置項
    options = Object.assign({ type: "sync", fn: fn }, options);
    // 將訂閱的事件存儲在taps裏面
    this._insert(options);
}

_insert(item) {
    // 將item 推動 this.taps
    this.taps[i] = item;
}
複製代碼

而後來看下Hook基礎類的call方法是如何實現的。

// node_modules/tapable/lib/Hook.js
class Hook {
    constructor(args) {
    	this.taps = [];
    	this.call = this._call;
    }

    compile(options) {
    	// 繼承類必須重寫compile
    	throw new Error("Abstract: should be overriden");
    }
    
    // 執行compile生成call方法
    _createCall(type) {
    	return this.compile({
            taps: this.taps,
    		// ...等參數
    	});
    }
}

// 動態生成call方法
function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
    	// 創造call等函數
    	this[name] = this._createCall(type);
    	// 執行觸發call等函數
    	return this[name](...args);
    };
}

// 定義_call方法
Object.defineProperties(Hook.prototype, {
    _call: {
    	value: createCompileDelegate("call", "sync"),
    	configurable: true,
    	writable: true
    },
});
複製代碼

經過上述代碼,咱們能夠發現,call方法到底是什麼,是經過重寫的compile方法生成出來的。那咱們再看下compile方法究竟作了什麼。

先來看下SyncHook的所有代碼。

// node_modules/tapable/lib/SyncHook.js
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

// 繼承工廠類
class SyncHookCodeFactory extends HookCodeFactory {
    // call方法個性化定製
    content({ onError, onDone, rethrowIfPossible }) {
    	return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
    	});
    }
}

const factory = new SyncHookCodeFactory();

// 繼承基礎Hook類
class SyncHook extends Hook {
    // 重寫Hook的compile方法
    compile(options) {
        // 開發者訂閱的事件傳
        factory.setup(this, options);
        // 動態生成call方法
    	return factory.create(options);
    }
}

module.exports = SyncHook;
複製代碼

能夠看到compile主要是執行factory的方法,而factorySyncHookCodeFactory的實例,繼承了HookCodeFactory類,而後factory實例調用了setup方法。

setup就是將taps中訂閱的事件方法統一給了this._x;

// node_modules/tapable/lib/HookCodeFactory.js
setup(instance, options) {
    // 將taps裏的全部fn 賦值給 _x
    instance._x = options.taps.map(t => t.fn);
}
複製代碼

而後再看下factory實例調用的create方法。

// node_modules/tapable/lib/HookCodeFactory.js
create(options) {
    this.init(options);
    let fn;
    switch (this.options.type) {
        case "sync":
            fn = new Function(
                // 參數
                this.args(),
                // 函數體
                '"use strict";\n' +
                // 獲取一些須要的變量
                this.header() +
                // 事件運行邏輯
                this.content({
                    onError: err => `throw ${err};\n`,
                    onResult: result => `return ${result};\n`,
                    resultReturns: true,
                    onDone: () => "",
                    rethrowIfPossible: true
                })
        );
        break;
    }
}
複製代碼

create會將傳進來的全部事件,進行組裝。最終生成call方法。 以下就是咱們此次的案例最終生成的call方法。

function anonymous(param1) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(param1);
    var _fn1 = _x[1];
    _fn1(param1);
    var _fn2 = _x[2];
    _fn2(param1);
}
複製代碼

若是你訂閱了5個事件,上述代碼就會變成5個函數的依次執行。以及參數必須是建立hook實例就聲明好的。不然tap事件傳的參數是無用的~

以上代碼仍是簡寫了不少,你們能夠直接去看下源碼,很是精簡好理解。給做者大大點贊。👍

總結一下,核心就是calltap兩個方法。其實還有tapAsync等...可是原理都是同樣的。tap收集訂閱的事件,觸發call方法時根據hook的種類動態生成對應的執行體。以下圖,其餘hook的實現也是同理。

Hook設計原理

5、Tapable在Webpack中的應用

Webpack的流程能夠分爲如下三大階段:

執行webpack時,會生成一個compiler實例。

// node_modules/webpack/lib/webpack.js
const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");

const webpack = (options, callback) => {
	// ...省略了多餘代碼...
    let compiler;
    if (typeof options === "object") {
    	compiler = new Compiler(options.context);
    } else {
    	throw new Error("Invalid argument: options");
    }
})
複製代碼

咱們發現Compiler是繼承了Tapable的。同時發現webpack的生命週期hooks都是各類各樣的鉤子。

// node_modules/webpack/lib/Compiler.js
class Compiler extends Tapable {
    constructor(context) {
    super();
        this.hooks = {
            /** @type {AsyncSeriesHook<Stats>} */
            done: new AsyncSeriesHook(["stats"]),
            /** @type {AsyncSeriesHook<>} */
            additionalPass: new AsyncSeriesHook([]),
            /** @type {AsyncSeriesHook<Compiler>} */
            beforeRun: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compiler>} */
            run: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            emit: new AsyncSeriesHook(["compilation"]),
            /** @type {AsyncSeriesHook<string, Buffer>} */
            assetEmitted: new AsyncSeriesHook(["file", "content"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            afterEmit: new AsyncSeriesHook(["compilation"]),
        
            // ....等等等不少 你們看下源碼吧.... 不看也沒有關係
        }
    }
}
複製代碼

而後在初始化webpack的配置過程當中,會循環咱們配置的以及webpack默認的全部插件也就是plugin

// 訂閱在options中的全部插件
if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else {
            plugin.apply(compiler);
        }
    }
}
複製代碼

這個過程,會把plugin中全部tap事件收集到每一個生命週期的hook中。 最後根據每一個hook執行call方法的順序(也就是生命週期)。就能夠把全部plugin執行了。

舉個例子,下面是咱們常用的熱更新插件代碼,它訂閱了additionalPasshook

熱更新插件
這也就是 webpack它工做流程能將各個插件 plugin串聯起來的緣由,而實現這一切的核心就是 Tapable

6、吐個槽

雖然插件化設計很靈活,咱們能夠寫插件操做webpack的整個生命週期。可是也發現插件化設計帶來的一些問題,就是閱讀源碼很是很差的體驗:

(1)聯繫鬆散。使用tapable鉤子相似事件監聽模式,雖然能有效解耦,但鉤子的註冊與調用幾乎沒有聯繫。

(2)看到源碼裏一個模塊提供了幾個鉤子,但並不知道,在什麼時候、何地該鉤子會被調用,又在什麼時候、何地鉤子上被註冊了哪些方法。這些以往都是須要經過在代碼庫中搜索關鍵詞來解決。

(3)鉤子數量衆多。webpack內部的鉤子很是多,數量達到了180+

參考連接

本篇文主要是講原理,理解tapable。其餘的鉤子的使用,能夠看這篇文章。

相關文章
相關標籤/搜索