Tapable
Tapable 包暴露了许多 Hook 类,这些类可用于创建插件的钩子。
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
安装
npm install --save tapable
用法
所有 Hook 构造函数都接受一个可选参数,这个参数是一个字符串数组,表示参数名称。
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
最佳实践是将一个类的所有 hooks 暴露在一个 hooks 属性中:
class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}
/* ... */
}
其他人现在可以使用这些 hooks:
const myCar = new Car();
// 使用 tap 方法添加一个消费者
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
必须传递一个名称来识别插件/原因。
你可能会接收参数:
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`加速到 ${newSpeed}`));
对于同步 hooks,tap 是唯一有效的添加插件的方法。异步 hooks 也支持异步插件:
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.findRoute(source, target, (err, route) => {
if(err) return callback(err);
routesList.add(route);
// 调用回调函数
callback();
});
});
// 你仍然可以使用同步插件
myCar.hooks.calculateRoutes.tap("CachedRoutesPlugin", (source, target, routesList) => {
const cachedRoute = cache.get(source, target);
if(cachedRoute)
routesList.add(cachedRoute);
})
声明这些 hooks 的类需要调用它们:
class Car {
/**
* 你不会从 SyncHook 或 AsyncParallelHook 中得到返回值,
* 要做到这一点,请使用 SyncWaterfallHook 和 AsyncSeriesWaterfallHook
**/
setSpeed(newSpeed) {
// 调用此方法即使你返回了值也会返回 undefined
this.hooks.accelerate.call(newSpeed);
}
useNavigationSystemPromise(source, target) {
const routesList = new List();
return this.hooks.calculateRoutes.promise(source, target, routesList).then((res) => {
// 对于 AsyncParallelHook,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());
});
}
}
Hook 将编译一个最有效的运行插件的方法。它根据以下情况生成代码:
- 注册的插件数量(没有,一个,多个)
- 注册的插件类型(同步,异步,Promise)
- 使用的调用方法(同步,异步,Promise)
- 参数数量
- 是否使用拦截
这确保了尽可能快的执行。
Hook 类型
每个 hook 可以被一个或多个函数注册。它们如何执行取决于 hook 类型:
-
基本 hook(名称中不包含“Waterfall”、“Bail”或“Loop”)。这个 hook 简单地依次调用它注册的每个函数。
-
Waterfall。一个 waterfall hook 也依次调用每个注册的函数。与基本 hook 不同,它将每个函数的返回值传递给下一个函数。
-
Bail。一个 bail hook 允许提前退出。当任何注册的函数返回任何值时,bail hook 将停止执行其余的函数。
-
Loop。当循环 hook 中的插件返回非 undefined 值时,hook 将从第一个插件重新启动。它将循环直到所有插件返回 undefined。
此外,hooks 可以是同步的或异步的。为了反映这一点,有“Sync”、“AsyncSeries”和“AsyncParallel” hook 类:
-
Sync。一个同步 hook 只能被同步函数注册(使用 myHook.tap())。
-
AsyncSeries。一个异步串行 hook 可以被同步、基于回调的和基于 Promise 的函数注册(使用 myHook.tap()、myHook.tapAsync() 和 myHook.tapPromise())。它们依次调用每个异步方法。
-
AsyncParallel。一个异步并行 hook 也可以被同步、基于回调的和基于 Promise 的函数注册(使用 myHook.tap()、myHook.tapAsync() 和 myHook.tapPromise())。然而,它们并行运行每个异步方法。
hook 类型体现在其类名中。例如,AsyncSeriesWaterfallHook 允许异步函数,并在串行运行它们,将每个函数的返回值传递给下一个函数。
拦截
所有 Hooks 提供额外的拦截 API:
myCar.hooks.calculateRoutes.intercept({
call: (source, target, routesList) => {
console.log("开始计算路线");
},
register: (tapInfo) => {
// tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
console.log(`${tapInfo.name} 正在执行它的工作`);
return tapInfo; // 可能返回一个新的 tapInfo 对象
}
})
- call: (…args) => void 将 call 添加到你的拦截器中将在触发 hook 时触发。你
可以访问 hook 的参数。
-
tap: (tap: Tap) => void 将 tap 添加到你的拦截器中将在插件注册到 hook 时触发。提供的是 Tap 对象。Tap 对象不能被改变。
-
loop: (…args) => void 将 loop 添加到你的拦截器中将在循环 hook 的每个循环时触发。
-
register: (tap: Tap) => Tap | undefined 将 register 添加到你的拦截器中将在每个添加的 Tap 时触发,并允许修改它。
上下文
插件和拦截器可以选择访问一个可选的上下文对象,这个对象可以用来传递任意值给后续的插件和拦截器。
myCar.hooks.accelerate.intercept({
context: true,
tap: (context, tapInfo) => {
// tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
console.log(`${tapInfo.name} 正在执行它的工作`);
// 如果至少有一个插件使用了 `context: true`,`context` 将作为空对象开始。
// 如果没有插件使用 `context: true`,那么 `context` 将是 undefined。
if (context) {
// 可以向 `context` 添加任意属性,插件可以访问这些属性。
context.hasMuffler = true;
}
}
});
myCar.hooks.accelerate.tap({
name: "NoisePlugin",
context: true
}, (context, newSpeed) => {
if (context && context.hasMuffler) {
console.log("安静...");
} else {
console.log("嗡嗡!");
}
});
HookMap
HookMap 是一个帮助类,用于包含 Hooks 的 Map。
const keyedHook = new HookMap(key => new SyncHook(["arg"]))
keyedHook.for("some-key").tap("MyPlugin", (arg) => { /* ... */ });
keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ });
keyedHook.for("some-key").tapPromise("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,
intercept: (interceptor: HookMapInterceptor) => void
}
interface HookMapInterceptor {
factory: (key: any, hook: Hook) => Hook
}
interface Tap {
name: string,
type: string
fn: Function,
stage: number,
context: boolean,
before?: string | Array
}
受保护的(仅对包含 hook 的类):
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 的类,用于将 taps 重定向到多个其他 hooks:
const { MultiHook } = require("tapable");
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);