Webpack 基石 tapable 揭祕

Webpack 基於 tapable 構建了其複雜龐大的流程管理系統,基於 tapable 的架構不只解耦了流程節點和流程的具體實現,還保證了 Webpack 強大的擴展能力;學習掌握tapable,有助於咱們深刻理解 Webpack。javascript

1、tapable是什麼?

The tapable package expose many Hook classes,which can be used to create hooks for plugins.java

tapable 提供了一些用於建立插件的鉤子類。ios

我的以爲 tapable 是一個基於事件的流程管理工具。ajax

2、tapable架構原理和執行過程

tapable於2020.9.18發佈了v2.0版本。此文章內容也是基於v2.0版本。axios

2.1 代碼架構

tapable有兩個基類:Hook和HookCodeFactory。Hook類定義了Hook interface(Hook接口), HookCodeFactoruy類的做用是動態生成一個流程控制函數。生成函數的方式是經過咱們熟悉的New Function(arg,functionBody)。api

Webpack 基石 tapable 揭祕

2.2 執行流程

tapable會動態生成一個可執行函數來控制鉤子函數的執行。咱們以SyncHook的使用來舉一個例子,好比咱們有這樣的一段代碼:數組

// SyncHook使用
import { SyncHook } from '../lib';
const syncHook = new SyncHook();
syncHook.tap('x', () => console.log('x done'));
syncHook.tap('y', () => console.log('y done'));

上面的代碼只是註冊好了鉤子函數,要讓函數被執行,還須要觸發事件(執行調用)promise

syncHook.call();

syncHook.call()在調用時會生成這樣的一個動態函數:架構

function anonymous() {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0();
    var _fn1 = _x[1];
    _fn1();
}

這個函數的代碼很是簡單:就是從一個數組中取出函數,依次執行。注意:不一樣的調用方式,最終生成的的動態函數是不一樣的。若是把調用代碼改爲:異步

syncHook.callAsync( () => {console.log('all done')} )

那麼最終生成的動態函數是這樣的:

function anonymous(_callback) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    var _hasError0 = false;
    try {
        _fn0();
    } catch(_err) {
        _hasError0 = true;
        _callback(_err);
    }
    if(!_hasError0) {
        var _fn1 = _x[1];
        var _hasError1 = false;
        try {
            _fn1();
        } catch(_err) {
            _hasError1 = true;
            _callback(_err);
        }
        if(!_hasError1) {
            _callback();
        }
    }
}

這個動態函數相對於前面的動態函數要複雜一些,但仔細一看,執行邏輯也很是簡單:一樣是從數組中取出函數,依次執行;只不過此次多了2個邏輯:

  • 錯誤處理
  • 在數組中的函數執行完後,執行了回調函數

經過研究最終生成的動態函數,咱們不難發現:動態函數的模板特性很是突出。前面的例子中,咱們只註冊了x,y2個鉤子,這個模板保證了當咱們註冊任意個鉤子時,動態函數也能方便地生成出來,具備很是強的擴展能力。

那麼這些動態函數是如何生成的呢?其實Hook的生成流程是同樣的。hook.tap只是完成參數準備,真正的動態函數生成是在調用後(水龍頭打開後)。完整流程以下:

Webpack 基石 tapable 揭祕

3、Hook 類型詳解

在tapablev2中,一共提供了12種類型的Hook,接下來,經過梳理Hook怎麼執行和Hook完成回調什麼時候執行2方面來理解tapable提供的這些Hook類。

3.1 SyncHook

鉤子函數按次序依次所有執行;若是有Hook回調,則Hook回調在最後執行。

const syncHook = new SyncHook();
syncHook.tap('x', () => console.log('x done'));
syncHook.tap('y', () => console.log('y done'));
syncHook.callAsync(() => { console.log('all done') });

/*
輸出:
x done
y done
all done
*/

3.2 SyncBailHook

鉤子函數按次序執行。若是某一步鉤子返回了非undefined,則後面的鉤子再也不執行;若是有Hook回調,直接執行Hook回調。

const hook = new SyncBailHook();

hook.tap('x', () => {
  console.log('x done');
  return false; // 返回了非undefined,y不會執行
});
hook.tap('y', () => console.log('y done'));
hook.callAsync(() => { console.log('all done') });

/*
輸出:
x done
all done
*/

3.3 SyncWaterfallHook

鉤子函數按次序所有執行。後一個鉤子的參數是前一個鉤子的返回值。最後執行Hook回調。

const hook = new SyncWaterfallHook(['count']);

hook.tap('x', (count) => {
    let result = count + 1;
    console.log('x done', result);
    return result;
});
hook.tap('y', (count) => {
    let result = count * 2;
    console.log('y done', result);
    return result;
});
hook.tap('z', (count) => {
    console.log('z done & show result', count);
});
hook.callAsync(5, () => { console.log('all done') });

/*
輸出:
x done 6
y done 12
z done & show result 12
all done
*/

3.4 SyncLoopHook

鉤子函數按次序所有執行。每一步的鉤子都會循環執行,直到返回值爲undefined,再開始執行下一個鉤子。Hook回調最後執行。

const hook = new SyncLoopHook();

let flag = 0;
let flag1 = 5;

hook.tap('x', () => {
    flag = flag + 1;

    if (flag >= 5) { // 執行5次,再執行 y
        console.log('x done');
        return undefined;
    } else {
        console.log('x loop');
        return true;
    }
});
hook.tap('y', () => {
    flag1 = flag1 * 2;

    if (flag1 >= 20) { // 執行2次,再執行 z
        console.log('y done');
        return undefined;
    } else {
        console.log('y loop');
        return true;
    }
});
hook.tap('z', () => {
    console.log('z done'); // z直接返回了undefined,因此只執行1次
    return undefined;
});

hook.callAsync(() => { console.log('all done') });

/*
輸出:
x loop
x loop
x loop
x loop
x done
y loop
x done
y done
z done
all done
 */

3.5  AsyncParallelHook

鉤子函數異步並行所有執行。全部鉤子的回調返回後,Hook回調才執行。

const hook = new AsyncParallelHook(['arg1']);
const start = Date.now();

hook.tapAsync('x', (arg1, callback) => {
    console.log('x done', arg1);

    setTimeout(() => {
        callback();
    }, 1000)
});
hook.tapAsync('y', (arg1, callback) => {
    console.log('y done', arg1);

    setTimeout(() => {
        callback();
    }, 2000)
});
hook.tapAsync('z', (arg1, callback) => {
    console.log('z done', arg1);

    setTimeout(() => {
        callback();
    }, 3000)
});

hook.callAsync(1, () => {
    console.log(`all done。 耗時:${Date.now() - start}`);
});

/*
輸出:
x done 1
y done 1
z done 1
all done。 耗時:3006
*/

3.6 AsyncSeriesHook

鉤子函數異步串行所有執行,會保證鉤子執行順序,上一個鉤子結束後,下一個纔會開始。Hook回調最後執行。

const hook = new AsyncSeriesHook(['arg1']);
const start = Date.now();

hook.tapAsync('x', (arg1, callback) => {
    console.log('x done', ++arg1);

    setTimeout(() => {
        callback();
    }, 1000)
});
hook.tapAsync('y', (arg1, callback) => {
    console.log('y done', arg1);

    setTimeout(() => {
        callback();
    }, 2000)
});

hook.tapAsync('z', (arg1, callback) => {
    console.log('z done', arg1);

    setTimeout(() => {
        callback();
    }, 3000)
});

hook.callAsync(1, () => {
    console.log(`all done。 耗時:${Date.now() - start}`);
});

/*
輸出:
x done 2
y done 1
z done 1
all done。 耗時:6008
*/

3.7 AsyncParallelBailHook

鉤子異步並行執行,即鉤子都會執行,但只要有一個鉤子返回了非undefined,Hook回調會直接執行。

const hook = new AsyncParallelBailHook(['arg1']);
const start = Date.now();

hook.tapAsync('x', (arg1, callback) => {
    console.log('x done', arg1);

    setTimeout(() => {
        callback();
    }, 1000)
});
hook.tapAsync('y', (arg1, callback) => {
    console.log('y done', arg1);

    setTimeout(() => {
        callback(true);
    }, 2000)
});

hook.tapAsync('z', (arg1, callback) => {
    console.log('z done', arg1);

    setTimeout(() => {
        callback();
    }, 3000)
});

hook.callAsync(1, () => {
    console.log(`all done。 耗時:${Date.now() - start}`);
});
/*
輸出:
x done 1
y done 1
z done 1
all done。 耗時:2006
 */

3.8 AsyncSeriesBailHook

鉤子函數異步串行執行。但只要有一個鉤子返回了非undefined,Hook回調就執行,也就是說有的鉤子可能不會執行。

const hook = new AsyncSeriesBailHook(['arg1']);
const start = Date.now();

hook.tapAsync('x', (arg1, callback) => {
    console.log('x done', ++arg1);

    setTimeout(() => {
        callback(true); // y 不會執行
    }, 1000);
});
hook.tapAsync('y', (arg1, callback) => {
    console.log('y done', arg1);

    setTimeout(() => {
        callback();
    }, 2000);
});

hook.callAsync(1, () => {
    console.log(`all done。 耗時:${Date.now() - start}`);
});

/*
輸出:
x done 2
all done。 耗時:1006
 */

3.9 AsyncSeriesWaterfallHook

鉤子函數異步串行所有執行,上一個鉤子返回的參數會傳給下一個鉤子。Hook回調會在全部鉤子回調返回後才執行。

const hook = new AsyncSeriesWaterfallHook(['arg']);
const start = Date.now();

hook.tapAsync('x', (arg, callback) => {
    console.log('x done', arg);

    setTimeout(() => {
        callback(null, arg + 1);
    }, 1000)
},);

hook.tapAsync('y', (arg, callback) => {
    console.log('y done', arg);

    setTimeout(() => {
        callback(null, true); // 不會阻止 z 的執行
    }, 2000)
});

hook.tapAsync('z', (arg, callback) => {
    console.log('z done', arg);
    callback();
});

hook.callAsync(1, (x, arg) => {
    console.log(`all done, arg: ${arg}。 耗時:${Date.now() - start}`);
});

/*
輸出:
x done 1
y done 2
z done true
all done, arg: true。 耗時:3010
 */

3.10 AsyncSeriesLoopHook

鉤子函數異步串行所有執行,某一步鉤子函數會循環執行到返回非undefined,纔會開始下一個鉤子。Hook回調會在全部鉤子回調完成後執行。

const hook = new AsyncSeriesLoopHook(['arg']);
const start = Date.now();
let counter = 0;

hook.tapAsync('x', (arg, callback) => {
    console.log('x done', arg);
    counter++;

    setTimeout(() => {
        if (counter >= 5) {
            callback(null, undefined); // 開始執行 y
        } else {
            callback(null, ++arg); // callback(err, result)
        }
    }, 1000)
},);

hook.tapAsync('y', (arg, callback) => {
    console.log('y done', arg);

    setTimeout(() => {
        callback(null, undefined);
    }, 2000)
});

hook.tapAsync('z', (arg, callback) => {
    console.log('z done', arg);
    callback(null, undefined);
});

hook.callAsync('AsyncSeriesLoopHook', (x, arg) => {
    console.log(`all done, arg: ${arg}。 耗時:${Date.now() - start}`);
});

/*
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
y done AsyncSeriesLoopHook
z done AsyncSeriesLoopHook
all done, arg: undefined。 耗時:7014
*/

3.11 HookMap

主要做用是Hook分組,方便Hook組批量調用。

const hookMap = new HookMap(() => new SyncHook(['x']));

hookMap.for('key1').tap('p1', function() {
    console.log('key1-1:', ...arguments);
});
hookMap.for('key1').tap('p2', function() {
    console.log('key1-2:', ...arguments);
});
hookMap.for('key2').tap('p3', function() {
    console.log('key2', ...arguments);
});

const hook = hookMap.get('key1');

if( hook !== undefined ) {
    hook.call('hello', function() {
        console.log('', ...arguments)
    });
}

/*
輸出:
key1-1: hello
key1-2: hello
*/

3.12 MultiHook

MultiHook主要用於向Hook批量註冊鉤子函數。

const syncHook = new SyncHook(['x']);
const syncLoopHook = new SyncLoopHook(['y']);
const mutiHook = new MultiHook([syncHook, syncLoopHook]);

// 向多個hook註冊同一個函數
mutiHook.tap('plugin', (arg) => {
    console.log('common plugin', arg);
});

// 執行函數
for (const hook of mutiHook.hooks) {
    hook.callAsync('hello', () => {
        console.log('hook all done');
    });
}

以上Hook又能夠抽象爲如下幾類:

  • xxxBailHook:根據前一步鉤子函數的返回值是不是undefined來決定要不要執行下一步鉤子:若是某一步返回了非undefined,則後面的鉤子不在執行。

  • xxxWaterfallHook:上一步鉤子函數返回值就是下一步函數的參數。

  • xxxLoopHook:鉤子函數循環執行,直到返回值爲undefined。

注意鉤子函數返回值判斷是和undefined對比,而不是和假值對比(null, false)

Hook也能夠按同步、異步劃分:

  • syncXXX:同步鉤子

  • asyncXXX:異步鉤子

Hook實例默認都有都有tap, tapAsync, tapPromise三個註冊鉤子回調的方法,不一樣註冊方法生成的動態函數是不同的。固然也並非全部Hook都支持這幾個方法,好比SyncHook不支持tapAsync, tapPromise。

Hook默認有call, callAsync,promise來執行回調。但並非全部Hook都會有這幾個方法,好比SyncHook不支持callAsync和promise。

4、實踐應用

4.1 基於 tapable 實現類 jQuery.ajax()封裝

咱們先複習下jQuery.ajax()的常規用法(大概用法是這樣,咱不糾結每一個參數都正確):

jQuery.ajax({
    url: 'api/request/url',
    beforeSend: function(config) {
        return config; // 返回false會取消這次請求發送
    },
    success: function(data) {
        // 成功邏輯
    }
    error: function(err) {
        // 失敗邏輯
    },
    complete: function() {
        // 成功,失敗都會執行的邏輯
    }
});

jQuery.ajax整個流程作了這麼幾件事:

  • 在請求真正發送前,beforeSend提供了請求配置預處理的鉤子。若是預處理函數返回false,能取消這次請求的發送。
  • 請求成功(服務端數據返回後)執行success函數邏輯。
  • 若是請求失敗,則執行error函數邏輯。
  • 最終,統一執行complete函數邏輯,不管請求成功仍是失敗。

Webpack 基石 tapable 揭祕

同時,咱們借鑑axios的作法,將beforeSend改成transformRequest,加入transformResponse,再加上統一的請求loading和默認的錯誤處理,這時咱們整個ajax流程以下:

Webpack 基石 tapable 揭祕

4.2 簡單版的實現

const { SyncHook, AsyncSeriesWaterfallHook } = require('tapable');

class Service {
    constructor() {
        this.hooks = {
            loading:  new SyncHook(['show']),
            transformRequest: new AsyncSeriesWaterfallHook(['config', 'transformFunction']),
            request: new SyncHook(['config']),
            transformResponse: new AsyncSeriesWaterfallHook(['config', 'response', 'transformFunction']),
            success: new SyncHook(['data']),
            fail: new SyncHook(['config', 'error']),
            finally: new SyncHook(['config', 'xhr'])
        };

        this.init();
    }
    init() {
        // 解耦後的任務邏輯
        this.hooks.loading.tap('LoadingToggle', (show) => {
            if (show) {
                console.log('展現ajax-loading');
            } else {
                console.log('關閉ajax-loading');
            }
        });

        this.hooks.transformRequest.tapAsync('DoTransformRequest', (
            config,
            transformFunction= (d) => {
                d.__transformRequest = true;
                return d;
            },
            cb
        ) => {
            console.log(`transformRequest攔截器:Origin:${JSON.stringify(config)};`);
            config = transformFunction(config);
            console.log(`transformRequest攔截器:after:${JSON.stringify(config)};`);
            cb(null, config);
        });

        this.hooks.transformResponse.tapAsync('DoTransformResponse', (
            config,
            data,
            transformFunction= (d) => {
                d.__transformResponse = true;
                return d;
            },
            cb
        ) => {
            console.log(`transformResponse攔截器:Origin:${JSON.stringify(config)};`);
            data = transformFunction(data);
            console.log(`transformResponse攔截器:After:${JSON.stringify(data)}`);
            cb(null, data);
        });

        this.hooks.request.tap('DoRequest', (config) => {
            console.log(`發送請求配置:${JSON.stringify(config)}`);

            // 模擬數據返回
            const sucData = {
                code: 0,
                data: {
                    list: ['X50 Pro', 'IQOO Neo'],
                    user: 'jack'
                },
                message: '請求成功'
            };

            const errData = {
                code: 100030,
                message: '未登陸,請從新登陸'
            };

            if (Date.now() % 2 === 0) {
                this.hooks.transformResponse.callAsync(config, sucData, undefined, () => {
                    this.hooks.success.callAsync(sucData, () => {
                        this.hooks.finally.call(config, sucData);
                    });
                });
            } else {
                this.hooks.fail.callAsync(config, errData, () => {
                    this.hooks.finally.call(config, errData);
                });
            }
        });
    }
    start(config) {
        this.config = config;

        /*
            經過Hook調用定製串聯流程
            1. 先 transformRequest
            2. 處理 loading
            3. 發起 request
         */
        this.hooks.transformRequest.callAsync(this.config, undefined, () => {
            this.hooks.loading.callAsync(this.config.loading, () => {
            });

            this.hooks.request.call(this.config);
        });
    }
}

const s = new Service();

s.hooks.success.tap('RenderList', (res) => {
    const { data } = res;
    console.log(`列表數據:${JSON.stringify(data.list)}`);
});

s.hooks.success.tap('UpdateUserInfo', (res) => {
    const { data } = res;
    console.log(`用戶信息:${JSON.stringify(data.user)}`);
});

s.hooks.fail.tap('HandlerError', (config, error) => {
    console.log(`請求失敗了,config=${JSON.stringify(config)},error=${JSON.stringify(error)}`);
});

s.hooks.finally.tap('DoFinally', (config, data) => {
    console.log(`DoFinally,config=${JSON.stringify(config)},data=${JSON.stringify(data)}`);
});

s.start({
    base: '/cgi/cms/',
    loading: true
});

/*
成功返回輸出:
transformRequest攔截器:Origin:{"base":"/cgi/cms/","loading":true};
transformRequest攔截器:after:{"base":"/cgi/cms/","loading":true,"__transformRequest":true};
展現ajax-loading
發送請求配置:{"base":"/cgi/cms/","loading":true,"__transformRequest":true}
transformResponse攔截器:Origin:{"base":"/cgi/cms/","loading":true,"__transformRequest":true};
transformResponse攔截器:After:{"code":0,"data":{"list":["X50 Pro","IQOO Neo"],"user":"jack"},"message":"請求成功","__transformResponse":true}
列表數據:["X50 Pro","IQOO Neo"]
用戶信息:"jack"
DoFinally,config={"base":"/cgi/cms/","loading":true,"__transformRequest":true},data={"code":0,"data":{"list":["X50 Pro","IQOO Neo"],"user":"jack"},"message":"請求成功","__transformResponse":true}
*/

上面的代碼,咱們能夠繼續優化:把每一個流程點都抽象成一個獨立插件,最後再串聯起來。如處理loading展現的獨立成LoadingPlugin.js,返回預處理transformResponse獨立成TransformResponsePlugin.js,這樣咱們可能獲得這麼一個結構:

Webpack 基石 tapable 揭祕

這個結構就和大名鼎鼎的Webpack組織插件的形式基本一致了。接下來咱們看看tapable在Webpack中的應用,看一看爲何tapable可以稱爲Webpack基石。

4.3 tapable在 Webpack中的應用

  • Webpack中,一切皆插件(Hook)。
  • Webpack經過tapable將這些插件串起來,組成固定流程。
  • tapable解耦了流程任務和具體實現,同時提供了強大的擴展能力:拿到Hook,就能插入本身的邏輯。(咱們平時寫Webpack插件,就是找到對應的Hook去,而後註冊咱們本身的鉤子函數。這樣就方便地把咱們自定義邏輯,插入到了Webpack任務流程中了)。

若是你須要強大的流程管理能力,能夠考慮基於tapable去作架構設計。

5、小結

  • tapable是一個流程管理工具。
  • 提供了10種類型Hook,能夠很方便地讓咱們去實現複雜的業務流程。
  • tapable核心原理是基於配置,經過new Function方式,實時動態生成函數表達式去執行,從而完成邏輯
  • tapable經過串聯流程節點來實現流程控制,保證了流程的準確有序。
  • 每一個流程節點能夠任意註冊鉤子函數,從而提供了強大的擴展能力。
  • tapable是Webpack基石,它支撐了Webpack龐大的插件系統,又保證了這些插件的有序運行。
  • 若是你也正在作一個複雜的流程系統(任務系統),能夠考慮用tapable來管理你的流程。

做者:vivo-Ou Fujun

相關文章
相關標籤/搜索