webpack核心模塊tapable用法解析
前不久寫了一篇webpack基本原理和AST用法的文章[1],本來想接著寫webpack plugin的原理的,但是發(fā)現(xiàn)webpack plugin高度依賴tapable[2]這個庫,不清楚tapable而直接去看webpack plugin始終有點霧里看花的意思。所以就先去看了下tapable的文檔和源碼,發(fā)現(xiàn)這個庫非常有意思,是增強版的發(fā)布訂閱模式。發(fā)布訂閱模式在源碼世界實在是太常見了,我們已經(jīng)在多個庫源碼里面見過了:
redux的subscribe和dispatch[3] Node.js的EventEmitter[4] redux-saga的take和put[5]
這些庫基本都自己實現(xiàn)了自己的發(fā)布訂閱模式,實現(xiàn)方式主要是用來滿足自己的業(yè)務(wù)需求,而tapable并沒有具體的業(yè)務(wù)邏輯,是一個專門用來實現(xiàn)事件訂閱或者他自己稱為hook(鉤子)的工具庫,其根本原理還是發(fā)布訂閱模式,但是他實現(xiàn)了多種形式的發(fā)布訂閱模式,還包含了多種形式的流程控制。
tapable暴露多個API,提供了多種流程控制方式,連使用都是比較復(fù)雜的,所以我想分兩篇文章來寫他的原理:
先看看用法,體驗下他的多種流程控制方式 通過用法去看看源碼是怎么實現(xiàn)的
本文就是講用法的文章,知道了他的用法,大家以后如果有自己實現(xiàn)hook或者事件監(jiān)聽的需求,可以直接拿過來用,非常強大!
本文例子已經(jīng)全部上傳到GitHub,大家可以拿下來做個參考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage[6]
tapable是什么
tapable是webpack的核心模塊,也是webpack團隊維護的,是webpack plugin的基本實現(xiàn)方式。他的主要功能是為使用者提供強大的hook機制,webpack plugin就是基于hook的。
主要API
下面是官方文檔中列出來的主要API,所有API的名字都是以Hook結(jié)尾的:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
這些API的名字其實就解釋了他的作用,注意這些關(guān)鍵字:Sync, Async, Bail, Waterfall, Loop, Parallel, Series。下面分別來解釋下這些關(guān)鍵字:
Sync:這是一個同步的hook
Async:這是一個異步的hook
Bail:Bail在英文中的意思是保險,保障的意思,實現(xiàn)的效果是,當(dāng)一個hook注冊了多個回調(diào)方法,任意一個回調(diào)方法返回了不為undefined的值,就不再執(zhí)行后面的回調(diào)方法了,就起到了一個“保險絲”的作用。
Waterfall:Waterfall在英語中是瀑布的意思,在編程世界中表示順序執(zhí)行各種任務(wù),在這里實現(xiàn)的效果是,當(dāng)一個hook注冊了多個回調(diào)方法,前一個回調(diào)執(zhí)行完了才會執(zhí)行下一個回調(diào),而前一個回調(diào)的執(zhí)行結(jié)果會作為參數(shù)傳給下一個回調(diào)函數(shù)。
Loop:Loop就是循環(huán)的意思,實現(xiàn)的效果是,當(dāng)一個hook注冊了回調(diào)方法,如果這個回調(diào)方法返回了true就重復(fù)循環(huán)這個回調(diào),只有當(dāng)這個回調(diào)返回undefined才執(zhí)行下一個回調(diào)。
Parallel:Parallel是并行的意思,有點類似于Promise.all,就是當(dāng)一個hook注冊了多個回調(diào)方法,這些回調(diào)同時開始并行執(zhí)行。
Series:Series就是串行的意思,就是當(dāng)一個hook注冊了多個回調(diào)方法,前一個執(zhí)行完了才會執(zhí)行下一個。
Parallel和Series的概念只存在于異步的hook中,因為同步hook全部是串行的。
下面我們分別來介紹下每個API的用法和效果。
同步API
同步API就是這幾個:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
} = require("tapable");
前面說了,同步API全部是串行的,所以這幾個的區(qū)別就在流程控制上。
SyncHook
SyncHook是一個最基礎(chǔ)的hook,其使用方法和效果接近我們經(jīng)常使用的發(fā)布訂閱模式,注意tapable導(dǎo)出的所有hook都是類,基本用法是這樣的:
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
因為SyncHook是一個類,所以使用new來生成一個實例,構(gòu)造函數(shù)接收的參數(shù)是一個數(shù)組["arg1", "arg2", "arg3"],這個數(shù)組有三項,表示生成的這個實例注冊回調(diào)的時候接收三個參數(shù)。實例hook主要有兩個實例方法:
tap:就是注冊事件回調(diào)的方法。call:就是觸發(fā)事件,執(zhí)行回調(diào)的方法。
下面我們擴展下官方文檔中小汽車加速的例子來說明下具體用法:
const { SyncHook } = require("tapable");
// 實例化一個加速的hook
const accelerate = new SyncHook(["newSpeed"]);
// 注冊第一個回調(diào),加速時記錄下當(dāng)前速度
accelerate.tap("LoggerPlugin", (newSpeed) =>
console.log("LoggerPlugin", `加速到 ${newSpeed}`)
);
// 再注冊一個回調(diào),用來檢測是否超速
accelerate.tap("OverspeedPlugin", (newSpeed) => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速??!");
}
});
// 再注冊一個回調(diào),用來檢測速度是否快到損壞車子了
accelerate.tap("DamagePlugin", (newSpeed) => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
});
// 觸發(fā)一下加速事件,看看效果吧
accelerate.call(500);
然后運行下看看吧,當(dāng)加速事件出現(xiàn)的時候,會依次執(zhí)行這三個回調(diào):

上面這個例子主要就是用了tap和call這兩個實例方法,其中tap接收兩個參數(shù),第一個是個字符串,并沒有實際用處,僅僅是一個注釋的作用,第二個參數(shù)就是一個回調(diào)函數(shù),用來執(zhí)行事件觸發(fā)時的具體邏輯。
accelerate.tap("LoggerPlugin", (newSpeed) =>
console.log("LoggerPlugin", `加速到 ${newSpeed}`)
);
上述這種寫法其實與webpack官方文檔中對于plugin的介紹非常像了[7],因為webpack的plguin就是用tapable實現(xiàn)的,第一個參數(shù)一般就是plugin的名字:

而call就是簡單的觸發(fā)這個事件,在webpack的plguin中一般不需要開發(fā)者去觸發(fā)事件,而是webpack自己在不同階段會觸發(fā)不同的事件,比如beforeRun, run等等,plguin開發(fā)者更多的會關(guān)注這些事件出現(xiàn)時應(yīng)該進(jìn)行什么操作,也就是在這些事件上注冊自己的回調(diào)。
SyncBailHook
上面的SyncHook其實就是一個簡單的發(fā)布訂閱模式,SyncBailHook就是在這個基礎(chǔ)上加了一點流程控制,前面我們說過了,Bail就是個保險,實現(xiàn)的效果是,前面一個回調(diào)返回一個不為undefined的值,就中斷這個流程。比如我們現(xiàn)在將前面這個例子的SyncHook換成SyncBailHook,然后在檢測超速的這個插件里面加點邏輯,當(dāng)它超速了就返回錯誤,后面的DamagePlugin就不會執(zhí)行了:
const { SyncBailHook } = require("tapable"); // 使用的是SyncBailHook
// 實例化一個加速的hook
const accelerate = new SyncBailHook(["newSpeed"]);
accelerate.tap("LoggerPlugin", (newSpeed) =>
console.log("LoggerPlugin", `加速到 ${newSpeed}`)
);
// 再注冊一個回調(diào),用來檢測是否超速
// 如果超速就返回一個錯誤
accelerate.tap("OverspeedPlugin", (newSpeed) => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速!!");
return new Error('您已超速!!');
}
});
accelerate.tap("DamagePlugin", (newSpeed) => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
});
accelerate.call(500);
然后再運行下看看:

可以看到由于OverspeedPlugin返回了一個不為undefined的值,DamagePlugin被阻斷,沒有運行了。
SyncWaterfallHook
SyncWaterfallHook也是在SyncHook的基礎(chǔ)上加了點流程控制,前面說了,Waterfall實現(xiàn)的效果是將上一個回調(diào)的返回值作為參數(shù)傳給下一個回調(diào)。所以通過call傳入的參數(shù)只會傳遞給第一個回調(diào)函數(shù),后面的回調(diào)接受都是上一個回調(diào)的返回值,最后一個回調(diào)的返回值會作為call的返回值返回給最外層:
const { SyncWaterfallHook } = require("tapable");
const accelerate = new SyncWaterfallHook(["newSpeed"]);
accelerate.tap("LoggerPlugin", (newSpeed) => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
return "LoggerPlugin";
});
accelerate.tap("Plugin2", (data) => {
console.log(`上一個插件是: ${data}`);
return "Plugin2";
});
accelerate.tap("Plugin3", (data) => {
console.log(`上一個插件是: ${data}`);
return "Plugin3";
});
const lastPlugin = accelerate.call(100);
console.log(`最后一個插件是:${lastPlugin}`);
然后看下運行效果吧:

SyncLoopHook
SyncLoopHook是在SyncHook的基礎(chǔ)上添加了循環(huán)的邏輯,也就是如果一個插件返回true就會一直執(zhí)行這個插件,直到他返回undefined才會執(zhí)行下一個插件:
const { SyncLoopHook } = require("tapable");
const accelerate = new SyncLoopHook(["newSpeed"]);
accelerate.tap("LoopPlugin", (newSpeed) => {
console.log("LoopPlugin", `循環(huán)加速到 ${newSpeed}`);
return new Date().getTime() % 5 !== 0 ? true : undefined;
});
accelerate.tap("LastPlugin", (newSpeed) => {
console.log("循環(huán)加速總算結(jié)束了");
});
accelerate.call(100);
執(zhí)行效果如下:

異步API
所謂異步API是相對前面的同步API來說的,前面的同步API的所有回調(diào)都是按照順序同步執(zhí)行的,每個回調(diào)內(nèi)部也全部是同步代碼。但是實際項目中,可能需要回調(diào)里面處理異步情況,也可能希望多個回調(diào)可以同時并行執(zhí)行,也就是Parallel。這些需求就需要用到異步API了,主要的異步API就是這些:
const {
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
既然涉及到了異步,那肯定還需要異步的處理方式,tapable支持回調(diào)函數(shù)和Promise兩種異步的處理方式。所以這些異步API除了用前面的tap來注冊回調(diào)外,還有兩個注冊回調(diào)的方法:tapAsync和tapPromise,對應(yīng)的觸發(fā)事件的方法為callAsync和promise。下面分別來看下每個API吧:
AsyncParallelHook
AsyncParallelHook從前面介紹的命名規(guī)則可以看出,他是一個異步并行執(zhí)行的Hook,我們先用tapAsync的方式來看下怎么用吧。
tapAsync和callAsync
還是那個小汽車加速的例子,只不過這個小汽車加速沒那么快了,需要一秒才能加速完成,然后我們在2秒的時候分別檢測是否超速和是否損壞,為了看出并行的效果,我們記錄下整個過程從開始到結(jié)束的時間:
const { AsyncParallelHook } = require("tapable");
const accelerate = new AsyncParallelHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
// 注意注冊異步事件需要使用tapAsync
// 接收的最后一個參數(shù)是done,調(diào)用他來表示當(dāng)前任務(wù)執(zhí)行完畢
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒后加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
done();
}, 1000);
});
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒后檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速?。?);
}
done();
}, 2000);
});
accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
// 2秒后檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
done();
}, 2000);
});
accelerate.callAsync(500, () => {
console.log("任務(wù)全部完成");
console.timeEnd("total time"); // 記錄總共耗時
});
上面代碼需要注意的是,注冊回調(diào)要使用tapAsync,而且回調(diào)函數(shù)里面最后一個參數(shù)會自動傳入done,你可以調(diào)用他來通知tapable當(dāng)前任務(wù)已經(jīng)完成。觸發(fā)任務(wù)需要使用callAsync,他最后也接收一個函數(shù),可以用來處理所有任務(wù)都完成后需要執(zhí)行的操作。所以上面的運行結(jié)果就是:

從這個結(jié)果可以看出,最終消耗的時間大概是2秒,也就是三個任務(wù)中最長的單個任務(wù)耗時,而不是三個任務(wù)耗時的總額,這就實現(xiàn)了Parallel并行的效果。
tapPromise和promise
現(xiàn)在都流行Promise,所以tapable也是支持的,執(zhí)行效果是一樣的,只是寫法不一樣而已。要用tapPromise,需要注冊的回調(diào)返回一個promise,同時觸發(fā)事件也需要用promise,任務(wù)運行完執(zhí)行的處理可以直接使用then,所以上述代碼改為:
const { AsyncParallelHook } = require("tapable");
const accelerate = new AsyncParallelHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
// 注意注冊異步事件需要使用tapPromise
// 回調(diào)函數(shù)要返回一個promise
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
return new Promise((resolve) => {
// 1秒后加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
resolve();
}, 1000);
});
});
accelerate.tapPromise("OverspeedPlugin", (newSpeed) => {
return new Promise((resolve) => {
// 2秒后檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速?。?);
}
resolve();
}, 2000);
});
});
accelerate.tapPromise("DamagePlugin", (newSpeed) => {
return new Promise((resolve) => {
// 2秒后檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
resolve();
}, 2000);
});
});
// 觸發(fā)事件使用promise,直接用then處理最后的結(jié)果
accelerate.promise(500).then(() => {
console.log("任務(wù)全部完成");
console.timeEnd("total time"); // 記錄總共耗時
});
這段代碼的邏輯和運行結(jié)果和上面那個是一樣的,只是寫法不一樣:

tapAsync和tapPromise混用
既然tapable支持這兩種異步寫法,那這兩種寫法可以混用嗎?我們來試試吧:
const { AsyncParallelHook } = require("tapable");
const accelerate = new AsyncParallelHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
// 來一個promise寫法
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
return new Promise((resolve) => {
// 1秒后加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
resolve();
}, 1000);
});
});
// 再來一個async寫法
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒后檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速??!");
}
done();
}, 2000);
});
// 使用promise觸發(fā)事件
// accelerate.promise(500).then(() => {
// console.log("任務(wù)全部完成");
// console.timeEnd("total time"); // 記錄總共耗時
// });
// 使用callAsync觸發(fā)事件
accelerate.callAsync(500, () => {
console.log("任務(wù)全部完成");
console.timeEnd("total time"); // 記錄總共耗時
});
這段代碼無論我是使用promise觸發(fā)事件還是callAsync觸發(fā)運行的結(jié)果都是一樣的,所以tapable內(nèi)部應(yīng)該是做了兼容轉(zhuǎn)換的,兩種寫法可以混用:

由于tapAsync和tapPromise只是寫法上的不一樣,我后面的例子就全部用tapAsync了。
AsyncParallelBailHook
前面已經(jīng)看了SyncBailHook,知道帶Bail的功能就是當(dāng)一個任務(wù)返回不為undefined的時候,阻斷后面任務(wù)的執(zhí)行。但是由于Parallel任務(wù)都是同時開始的,阻斷是阻斷不了了,實際效果是如果有一個任務(wù)返回了不為undefined的值,最終的回調(diào)會立即執(zhí)行,并且獲取Bail任務(wù)的返回值。我們將上面三個任務(wù)執(zhí)行時間錯開,分別為1秒,2秒,3秒,然后在2秒的任務(wù)觸發(fā)Bail就能看到效果了:
const { AsyncParallelBailHook } = require("tapable");
const accelerate = new AsyncParallelBailHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒后加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
done();
}, 1000);
});
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒后檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速??!");
}
// 這個任務(wù)的done返回一個錯誤
// 注意第一個參數(shù)是node回調(diào)約定俗成的錯誤
// 第二個參數(shù)才是Bail的返回值
done(null, new Error("您已超速!!"));
}, 2000);
});
accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
// 3秒后檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
done();
}, 3000);
});
accelerate.callAsync(500, (error, data) => {
if (data) {
console.log("任務(wù)執(zhí)行出錯:", data);
} else {
console.log("任務(wù)全部完成");
}
console.timeEnd("total time"); // 記錄總共耗時
});
可以看到執(zhí)行到任務(wù)2時,由于他返回了一個錯誤,所以最終的回調(diào)會立即執(zhí)行,但是由于任務(wù)3之前已經(jīng)同步開始了,所以他自己仍然會運行完,只是已經(jīng)不影響最終結(jié)果了:

AsyncSeriesHook
AsyncSeriesHook是異步串行hook,如果有多個任務(wù),這多個任務(wù)之間是串行的,但是任務(wù)本身卻可能是異步的,下一個任務(wù)必須等上一個任務(wù)done了才能開始:
const { AsyncSeriesHook } = require("tapable");
const accelerate = new AsyncSeriesHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒后加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
done();
}, 1000);
});
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒后檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速?。?);
}
done();
}, 2000);
});
accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
// 2秒后檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
done();
}, 2000);
});
accelerate.callAsync(500, () => {
console.log("任務(wù)全部完成");
console.timeEnd("total time"); // 記錄總共耗時
});
每個任務(wù)代碼跟AsyncParallelHook是一樣的,只是使用的Hook不一樣,而最終效果的區(qū)別是:AsyncParallelHook所有任務(wù)同時開始,所以最終總耗時就是耗時最長的那個任務(wù)的耗時;AsyncSeriesHook的任務(wù)串行執(zhí)行,下一個任務(wù)要等上一個任務(wù)完成了才能開始,所以最終總耗時是所有任務(wù)耗時的總和,上面這個例子就是1 + 2 + 2,也就是5秒:

AsyncSeriesBailHook
AsyncSeriesBailHook就是在AsyncSeriesHook的基礎(chǔ)上加上了Bail的邏輯,也就是中間任何一個任務(wù)返回不為undefined的值,終止執(zhí)行,直接執(zhí)行最后的回調(diào),并且將這個返回值傳給最終的回調(diào):
const { AsyncSeriesBailHook } = require("tapable");
const accelerate = new AsyncSeriesBailHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒后加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
done();
}, 1000);
});
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒后檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速??!");
}
// 這個任務(wù)的done返回一個錯誤
// 注意第一個參數(shù)是node回調(diào)約定俗成的錯誤
// 第二個參數(shù)才是Bail的返回值
done(null, new Error("您已超速!!"));
}, 2000);
});
accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
// 2秒后檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
done();
}, 2000);
});
accelerate.callAsync(500, (error, data) => {
if (data) {
console.log("任務(wù)執(zhí)行出錯:", data);
} else {
console.log("任務(wù)全部完成");
}
console.timeEnd("total time"); // 記錄總共耗時
});
這個執(zhí)行結(jié)果跟AsyncParallelBailHook的區(qū)別就是AsyncSeriesBailHook被阻斷后,后面的任務(wù)由于還沒開始,所以可以被完全阻斷,而AsyncParallelBailHook后面的任務(wù)由于已經(jīng)開始了,所以還會繼續(xù)執(zhí)行,只是結(jié)果已經(jīng)不關(guān)心了。

AsyncSeriesWaterfallHook
Waterfall的作用是將前一個任務(wù)的結(jié)果傳給下一個任務(wù),其他的跟AsyncSeriesHook一樣的,直接來看代碼吧:
const { AsyncSeriesWaterfallHook } = require("tapable");
const accelerate = new AsyncSeriesWaterfallHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒后加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
// 注意done的第一個參數(shù)會被當(dāng)做error
// 第二個參數(shù)才是傳遞給后面任務(wù)的參數(shù)
done(null, "LoggerPlugin");
}, 1000);
});
accelerate.tapAsync("Plugin2", (data, done) => {
setTimeout(() => {
console.log(`上一個插件是: ${data}`);
done(null, "Plugin2");
}, 2000);
});
accelerate.tapAsync("Plugin3", (data, done) => {
setTimeout(() => {
console.log(`上一個插件是: ${data}`);
done(null, "Plugin3");
}, 2000);
});
accelerate.callAsync(500, (error, data) => {
console.log("最后一個插件是:", data);
console.timeEnd("total time"); // 記錄總共耗時
});
運行效果如下:

總結(jié)
本文例子已經(jīng)全部上傳到GitHub,大家可以拿下來做個參考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage[8]
tapable是webpack實現(xiàn)plugin的核心庫,他為webpack提供了多種事件處理和流程控制的Hook。這些 Hook主要有同步(Sync)和異步(Async)兩種,同時還提供了阻斷(Bail),瀑布(Waterfall),循環(huán)(Loop)等流程控制,對于異步流程還提供了并行(Paralle)和串行(Series)兩種控制方式。tapable其核心原理還是事件的發(fā)布訂閱模式,他使用tap來注冊事件,使用call來觸發(fā)事件。異步 hook支持兩種寫法:回調(diào)和Promise,注冊和觸發(fā)事件分別使用tapAsync/callAsync和tapPromise/promise。異步 hook使用回調(diào)寫法的時候要注意,回調(diào)函數(shù)的第一個參數(shù)默認(rèn)是錯誤,第二個參數(shù)才是向外傳遞的數(shù)據(jù),這也符合node回調(diào)的風(fēng)格。
這篇文章主要講述了tapable的用法,后面我會寫一篇文章來分析他的源碼,點個關(guān)注不迷路,哈哈~
覺得博主寫得還可以的話,不要忘了分享、點贊、在看三連哦~
長按下方圖片,關(guān)注進(jìn)擊的大前端,獲取更多的優(yōu)質(zhì)原創(chuàng)文章~
參考資料
webpack基本原理和AST用法的文章: https://juejin.cn/post/6930877602840182791
[2]tapable: https://github.com/webpack/tapable
[3]redux的subscribe和dispatch: https://juejin.cn/post/6845166891682512909
Node.js的EventEmitter: https://juejin.cn/post/6844904101331877895
redux-saga的take和put: https://juejin.cn/post/6885223002703822855
https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage: https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage
[7]webpack官方文檔中對于plugin的介紹非常像了: https://www.webpackjs.com/concepts/plugins/#%E5%89%96%E6%9E%90
[8]https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage: https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage
[9]進(jìn)擊的大前端: https://test-dennis.oss-cn-hangzhou.aliyuncs.com/QRCode/QR430.jpg
[10]https://juejin.im/post/5e3ffc85518825494e2772fd: https://juejin.im/post/5e3ffc85518825494e2772fd
[11]https://github.com/dennis-jiang/Front-End-Knowledges: https://github.com/dennis-jiang/Front-End-Knowledges
