Tapable是一個為外掛創造鉤子的庫,他也是webpack的核心庫。Tapable v1之後的版本跟之前的用法差別非常大,主要區別是以前用繼承class A extends tapable, 現在直接在類裡面定義私有成員this.hooks. 貌似網上很多都是老版本的用法,鑑於馬上就要v2了,翻譯走一波,順便點一下目前1.1版本的坑
原文github.com/webpack/tap…
Tapable
tapable提供很多鉤子類(Hook classes),他們可以被用來為外掛創造鉤子。
const {
SyncHook, // 同步鉤子
SyncBailHook, // 同步早退鉤子
SyncWaterfallHook, // 同步瀑布鉤子
SyncLoopHook, // 同步迴圈鉤子
AsyncParallelHook, // 非同步併發鉤子
AsyncParallelBailHook, // 非同步併發可早退鉤子
AsyncSeriesHook, // 非同步順序鉤子
AsyncSeriesBailHook, // 非同步順序可早退鉤子
AsyncSeriesWaterfallHook // 非同步順序瀑布鉤子
} = require("tapable");複製程式碼
安裝
npm install --save 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();
// 使用tap方法新增具體的執行邏輯
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on()); // 亮燈外掛,邏輯為剎車時亮燈複製程式碼
為了定位你的外掛,一個合適的名字(上面WarningLampPlugin)是必須的。
你定義的函式可以接收引數
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));複製程式碼
對於同步的鉤子,tap是僅有的新增外掛的有效方法。非同步鉤子還支援非同步外掛,除了tap外,還有tapPromise,tapAsync等方法。
myCar.hooks.calculateRoutes.tapPromise("GoogleMapsPlugin", (source, target, routesList) => {
// 谷歌的找路線的非同步方法返回promise
return google.maps.findRoute(source, target).then(route => {
routesList.add(route);
});
});
myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin", (source, target, routesList, callback) => {
// bing的找路線非同步方法用的callback方式
bing.findRoute(source, target, (err, route) => {
if(err) return callback(err);
routesList.add(route);
// call the callback
callback();
});
});
// 非同步鉤子也可以使用同步方法,比如下例取出快取的版本
myCar.hooks.calculateRoutes.tap("CachedRoutesPlugin", (source, target, routesList) => {
const cachedRoute = cache.get(source, target);
if(cachedRoute)
routesList.add(cachedRoute);
})複製程式碼
然後宣告瞭這些鉤子的類需要用他們時:
class Car {
/* 我作為一輛車,我只在乎我有以下功能,但這些功能的具體實現交給了第三方,
* 我給這些第三方提供能修改邏輯的許可權就好了 */
setSpeed(newSpeed) {
// 下面的call沒有返回值
this.hooks.accelerate.call(newSpeed);
}
useNavigationSystemPromise(source, target) {
const routesList = new List();
return this.hooks.calculateRoutes.promise(source, target, routesList).then((res) => {
// res是undefined
return routesList.getRoutes();
});
}
useNavigationSystemAsync(source, target, callback) {
const routesList = new List();
this.hooks.calculateRoutes.callAsync(source, target, routesList, err => {
if(err) return callback(err);
callback(null, routesList.getRoutes());
});
}
}複製程式碼
(注:此處例子用的是SyncHook和AsyncParallelHook, 所以他們是沒有返回值的,即使你返回了也只能得到undefined。要想得到返回值請用SyncWaterfallHook和AsyncSeriesWaterfallHook!而且注意waterfall鉤子總會返回值(即使你不return))
我們會用最高效的方式編譯一個執行你提供的外掛的方法,生成的程式碼取決於:
- 註冊外掛的數量(0,1,多個)
- 註冊外掛的型別(同步,非同步回撥,非同步promise)
- 使用的呼叫方法(call, promise,callAsync)
- 引數的數量
- 是否用攔截器
這點特性保證了最快的執行。
鉤子型別
每個鉤子可以關聯多個函式,它們怎麼執行取決於鉤子型別:
- 基礎鉤子(名字裡沒有waterfall, bail, loop的):這種鉤子簡單地按順序呼叫每個新增的函式。
- 瀑布鉤子(waterfall):也會按順序呼叫函式,不同的是,他會傳遞每個函式的返回值到下一個函式。如果你不顯式地return值,那麼函式會返回你的第一個引數當返回值,所以記得總要返回一個值(我會return 'defined';)!
- 早退鉤子(bail):當有任何新增的函式返回了任何值,這種鉤子就會停止執行後面的函式。
- 迴圈鉤子(loop):還在開發中...
另外鉤子還分為同步或非同步:
- 同步(sync):同步鉤子只能新增同步函式(使用myHook.tap())
- 非同步序列(AsyncSeries):可以新增同步方法,基於回撥的非同步方法,基於promise的非同步方法(使用.tap(), .tapAsync(), .tapPromise())。按出現的順序呼叫新增的非同步方法。
- 非同步平行(AsyncParallel):跟上面一樣,只不過併發的呼叫新增的非同步方法。
你可以通過這些鉤子類的名字判斷他們的模型, 比如AsyncSeriesWaterfallHook代表按順序執行非同步方法,並且按順序傳遞返回值。
攔截器
所有的鉤子都提供攔截器介面:
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 當你的自定義外掛被新增進鉤子時觸發,此時你可以訪問這個tap物件,但只讀。
register: (tap: Tap) => Tap | undefined 當你的自定義外掛被新增進鉤子時觸發,此時你可以訪問這個tap物件,可修改並返回新的tap物件。
loop: (...args) => void 迴圈鉤子的每個迴圈都會被觸發。
上下文
外掛和攔截器可以可選地訪問上下文物件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` 從一個空物件開始如果至少有一個外掛裡寫了 `context: true`.
// 如果沒有外掛定義 `context: true`, 那麼 `context` 是 undefined.
if (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
這是一個鉤子的字典幫助類,比起你直接用js的字典類new Map([['key', hook]]),這個類可能用起來更簡單:
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 => { /* ... */ });
}複製程式碼
Hook/HookMap介面
公有的
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類
這是一個像鉤子的類,用來重定向鉤子的外掛到其他鉤子:
const { MultiHook } = require("tapable");
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);複製程式碼