Webpack 基於 tapable 構建了其複雜龐大的流程管理系統,基於 tapable 的架構不只解耦了流程節點和流程的具體實現,還保證了 Webpack 強大的擴展能力;學習掌握tapable,有助於咱們深刻理解 Webpack。javascript
The tapable package expose many Hook classes,which can be used to create hooks for plugins.tapable 提供了一些用於建立插件的鉤子類。java
我的以爲 tapable 是一個基於事件的流程管理工具。ios
tapable於2020.9.18發佈了v2.0版本。此文章內容也是基於v2.0版本。ajax
tapable有兩個基類:Hook和HookCodeFactory。Hook類定義了Hook interface(Hook接口), HookCodeFactoruy類的做用是動態生成一個流程控制函數。生成函數的方式是經過咱們熟悉的New Function(arg,functionBody)。axios
tapable會動態生成一個可執行函數來控制鉤子函數的執行。咱們以SyncHook的使用來舉一個例子,好比咱們有這樣的一段代碼:api
// SyncHook使用 import { SyncHook } from '../lib'; const syncHook = new SyncHook(); syncHook.tap('x', () => console.log('x done')); syncHook.tap('y', () => console.log('y done'));
上面的代碼只是註冊好了鉤子函數,要讓函數被執行,還須要觸發事件(執行調用)數組
syncHook.call();
syncHook.call()在調用時會生成這樣的一個動態函數:promise
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只是完成參數準備,真正的動態函數生成是在調用後(水龍頭打開後)。完整流程以下:
在tapablev2中,一共提供了12種類型的Hook,接下來,經過梳理Hook怎麼執行和Hook完成回調什麼時候執行2方面來理解tapable提供的這些Hook類。
鉤子函數按次序依次所有執行;若是有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 */
鉤子函數按次序執行。若是某一步鉤子返回了非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 */
鉤子函數按次序所有執行。後一個鉤子的參數是前一個鉤子的返回值。最後執行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 */
鉤子函數按次序所有執行。每一步的鉤子都會循環執行,直到返回值爲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 */
鉤子函數異步並行所有執行。全部鉤子的回調返回後,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 */
鉤子函數異步串行所有執行,會保證鉤子執行順序,上一個鉤子結束後,下一個纔會開始。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 */
鉤子異步並行執行,即鉤子都會執行,但只要有一個鉤子返回了非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 */
鉤子函數異步串行執行。但只要有一個鉤子返回了非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 */
鉤子函數異步串行所有執行,上一個鉤子返回的參數會傳給下一個鉤子。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 */
鉤子函數異步串行所有執行,某一步鉤子函數會循環執行到返回非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 */
主要做用是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 */
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又能夠抽象爲如下幾類:
注意鉤子函數返回值判斷是和undefined對比,而不是和假值對比(null, false)
Hook也能夠按同步、異步劃分:
Hook實例默認都有都有tap, tapAsync, tapPromise三個註冊鉤子回調的方法,不一樣註冊方法生成的動態函數是不同的。固然也並非全部Hook都支持這幾個方法,好比SyncHook不支持tapAsync, tapPromise。
Hook默認有call, callAsync,promise來執行回調。但並非全部Hook都會有這幾個方法,好比SyncHook不支持callAsync和promise。
咱們先複習下jQuery.ajax()的常規用法(大概用法是這樣,咱不糾結每一個參數都正確):
jQuery.ajax({ url: 'api/request/url', beforeSend: function(config) { return config; // 返回false會取消這次請求發送 }, success: function(data) { // 成功邏輯 } error: function(err) { // 失敗邏輯 }, complete: function() { // 成功,失敗都會執行的邏輯 } });
jQuery.ajax整個流程作了這麼幾件事:
同時,咱們借鑑axios的作法,將beforeSend改成transformRequest,加入transformResponse,再加上統一的請求loading和默認的錯誤處理,這時咱們整個ajax流程以下:
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去作架構設計。
做者:vivo-Ou Fujun