起因
搜尋引擎搜尋tapable中文文檔,你會看見各種翻譯,點進去一看,确實是官方的文檔翻譯過來的,但是webpack的文檔确實還有很多需要改進的地方,既然是開源的為什麼不去github上的tapable庫看呢,一看,确實,比webpack文檔上的描述得清楚得多.
tapable 是一個類似于nodejs 的EventEmitter 的庫, 主要是控制鈎子函數的釋出與訂閱,控制着webpack的插件系.webpack的本質就是一系列的插件運作.
Tapable
Tapable庫 提供了很多的鈎子類, 這些類可以為插件建立鈎子
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
安裝
npm install --save tapable
使用
所有的鈎子構造函數,都接受一個可選的參數,(這個參數最好是數組,不是tapable内部也把他變成數組),這是一個參數的字元串名字清單
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
最好的實踐就是把所有的鈎子暴露在一個類的hooks屬性裡面:
class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}
/* ... */
}
其他開發者現在可以這樣用這些鈎子
const myCar = new Car();
// Use the tap method to add a consument
// 使用tap 方法添加一個消費者,(生産者消費者模式)
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
這需要你傳一個名字去标記這個插件:
你可以接收參數
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
在同步鈎子中, tap 是唯一的綁定方法,異步鈎子通常支援異步插件
// promise: 綁定promise鈎子的API
myCar.hooks.calculateRoutes.tapPromise("GoogleMapsPlugin", (source, target, routesList) => {
// return a promise
return google.maps.findRoute(source, target).then(route => {
routesList.add(route);
});
});
// tapAsync:綁定異步鈎子的API
myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin", (source, target, routesList, callback) => {
bing.findRoute(source, target, (err, route) => {
if(err) return callback(err);
routesList.add(route);
// call the callback
callback();
});
});
// You can still use sync plugins
// tap: 綁定同步鈎子的API
myCar.hooks.calculateRoutes.tap("CachedRoutesPlugin", (source, target, routesList) => {
const cachedRoute = cache.get(source, target);
if(cachedRoute)
routesList.add(cachedRoute);
})
類需要調用被聲明的那些鈎子
class Car {
/* ... */
setSpeed(newSpeed) {
// call(xx) 傳參調用同步鈎子的API
this.hooks.accelerate.call(newSpeed);
}
useNavigationSystemPromise(source, target) {
const routesList = new List();
// 調用promise鈎子(鈎子傳回一個promise)的API
return this.hooks.calculateRoutes.promise(source, target, routesList).then(() => {
return routesList.getRoutes();
});
}
useNavigationSystemAsync(source, target, callback) {
const routesList = new List();
// 調用異步鈎子API
this.hooks.calculateRoutes.callAsync(source, target, routesList, err => {
if(err) return callback(err);
callback(null, routesList.getRoutes());
});
}
}
鈎子會用最有效率的方式去編譯(建構)一個運作你的插件的方法,他生成的代碼依賴于一下幾點:
- 你注冊的插件的個數.
- 你注冊插件的類型.
- 你使用的調用方法(call, promise, async) // 其實這個類型已經包括了
- 鈎子參數的個數 // 就是你new xxxHook(['ooo']) 傳入的參數
- 是否應用了攔截器(攔截器下面有講)
這些确定了盡可能快的執行.
鈎子類型
每一個鈎子都可以tap 一個或者多個函數, 他們如果運作,取決于他們的鈎子類型
- 基本的鈎子, (鈎子類名沒有waterfall, Bail, 或者 Loop 的 ), 這個鈎子隻會簡單的調用每個tap進去的函數
- Waterfall, 一個waterfall 鈎子,也會調用每個tap進去的函數,不同的是,他會從每一個函數傳一個傳回的值到下一個函數
- Bail, Bail 鈎子允許更早的退出,當任何一個tap進去的函數,傳回任何值, bail類會停止執行其他的函數執行.(類似 Promise.reace())
- Loop, TODO(我.... 這裡也沒描述,應該是寫文檔得時候 還沒想好這個要怎麼寫,我嘗試看他代碼去補全,不過可能需要點時間.)
此外,鈎子可以是同步的,也可以是異步的,Sync, AsyncSeries 和 AsyncParallel 鈎子就反應了這個問題
- Sync, 一個同步鈎子隻能tap同步函數, 不然會報錯.
- AsyncSeries, 一個 async-series 鈎子 可以tap 同步鈎子, 基于回調的鈎子(我估計是類似chunk的東西)和一個基于promise的鈎子(使用
,myHook.tap()
和myHook.tapAsync()
.).他會按順序的調用每個方法.myHook.tapPromise()
- AsyncParallel, 一個 async-parallel 鈎子跟上面的 async-series 一樣 不同的是他會把異步鈎子并行執行(并行執行就是把異步鈎子全部一起開啟,不按順序執行).
攔截器(interception)
所有鈎子都提供額外的攔截器API
// 注冊一個攔截器
myCar.hooks.calculateRoutes.intercept({
call: (source, target, routesList) => {
console.log("Starting to calculate routes");
},
register: (tapInfo) => {
// tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
console.log(`${tapInfo.name} is doing its job`);
return tapInfo; // may return a new tapInfo object
}
})
call:
(...args) => void
當你的鈎子觸發之前,(就是call()之前),就會觸發這個函數,你可以通路鈎子的參數.多個鈎子執行一次
tap:
(tap: Tap) => void
每個鈎子執行之前(多個鈎子執行多個),就會觸發這個函數
loop:
(...args) => void
這個會為你的每一個循環鈎子(LoopHook, 就是類型到Loop的)觸發,具體什麼時候沒說
register:
(tap: Tap) => Tap | undefined
每添加一個
Tap
都會觸發 你interceptor上的register,你下一個攔截器的register 函數得到的參數 取決于你上一個register傳回的值,是以你最好傳回一個 tap 鈎子.
Context(上下文)
插件和攔截器都可以選擇加入一個可選的 context對象, 這個可以被用于傳遞随意的值到隊列中的插件和攔截器.
myCar.hooks.accelerate.intercept({
context: true,
tap: (context, tapInfo) => {
// tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
console.log(`${tapInfo.name} is doing it's job`);
// `context` starts as an empty object if at least one plugin uses `context: true`.
// 如果最少有一個插件使用 `context` 那麼context 一開始是一個空的對象
// If no plugins use `context: true`, then `context` is undefined
// 如過tap進去的插件沒有使用`context` 的 那麼内部的`context` 一開始就是undefined
if (context) {
// Arbitrary properties can be added to `context`, which plugins can then access.
// 任意屬性都可以添加到`context`, 插件可以通路到這些屬性
context.hasMuffler = true;
}
}
});
myCar.hooks.accelerate.tap({
name: "NoisePlugin",
context: true
}, (context, newSpeed) => {
if (context && context.hasMuffler) {
console.log("Silence...");
} else {
console.log("Vroom!");
}
});
HookMap
一個 HookMap是一個Hooks映射的幫助類
const keyedHook = new HookMap(key => new SyncHook(["arg"]))
keyedHook.tap("some-key", "MyPlugin", (arg) => { /* ... */ });
keyedHook.tapAsync("some-key", "MyPlugin", (arg, callback) => { /* ... */ });
keyedHook.tapPromise("some-key", "MyPlugin", (arg) => { /* ... */ });
const hook = keyedHook.get("some-key");
if(hook !== undefined) {
hook.callAsync("arg", err => { /* ... */ });
}
鈎子映射接口(HookMap interface)
Public(權限公開的):
interface Hook {
tap: (name: string | Tap, fn: (context?, ...args) => Result) => void,
tapAsync: (name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
tapPromise: (name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
intercept: (interceptor: HookInterceptor) => void
}
interface HookInterceptor {
call: (context?, ...args) => void,
loop: (context?, ...args) => void,
tap: (context?, tap: Tap) => void,
register: (tap: Tap) => Tap,
context: boolean
}
interface HookMap {
for: (key: any) => Hook,
tap: (key: any, name: string | Tap, fn: (context?, ...args) => Result) => void,
tapAsync: (key: any, name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
tapPromise: (key: any, name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
intercept: (interceptor: HookMapInterceptor) => void
}
interface HookMapInterceptor {
factory: (key: any, hook: Hook) => Hook
}
interface Tap {
name: string,
type: string
fn: Function,
stage: number,
context: boolean
}
Protected(保護的權限),隻用于類包含的(裡面的)鈎子
interface Hook {
isUsed: () => boolean,
call: (...args) => Result,
promise: (...args) => Promise<Result>,
callAsync: (...args, callback: (err, result: Result) => void) => void,
}
interface HookMap {
get: (key: any) => Hook | undefined,
for: (key: any) => Hook
}
MultiHook
把其他的Hook 重定向(轉化)成為一個 MultiHook
const { MultiHook } = require("tapable");
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
OK 所有的内容我都已翻譯完成.
其中有很多不是直譯,這樣寫下來感覺就是按照原文的脈絡重新寫了一遍....,應該能更清楚明白,要不是怕丢臉我就給個原創了,哈哈.
之後, 我還會寫一篇完整的原創解析,直擊源碼,搞定tapable, 完全了解webpack插件系統(webpack本來就是一個插件的事件流), 好久沒寫原創了. 我自己也很期待.
來源:
https://segmentfault.com/a/1190000017420937