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属性中公开类的所有hook:
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
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
需要传递一个名称来识别插件。
你可能会收到如下参数:
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
对于同步钩子,tap是添加插件的唯一有效方法。异步钩子也支持异步插件:
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);
})
声明这些钩子的类需要调用它们:
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) => {
// res在AsyncParallelHook中是undefind
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会以最有效的方式编译一个方法来运行插件。它生成的代码取决于:
注册插件的数量(无,一个,多个)
注册插件的类型(sync, async, promise)
所使用的调用方法(sync, async, promise)
参数的个数
是否使用拦截
这确保了尽可能快的执行。
Hook 类型
每个hook都可以使用一个或多个函数。它们如何执行取决于hook类型:
同步基本的钩子 (名称中没有“Waterfall”,“Bail”或“Loop”)。这个钩子只是依次调用它注册的每个函数,上一个函数执行完成,进行下一个函数。
Waterfal: waterfall hook 也会依次调用它注册的每个函数。与基本钩子不同,它将每个函数的返回值传递给下一个函数。
Bail: Bail hook 允许提前退出。当任何被选中的函数返回任何内容时,保释钩子将停止执行剩下的内容。
Loop: 当一个loop hook中的插件返回一个非undefined值时,钩子将从第一个插件重新启动。它会循环,直到所有插件返回undefined。
此外,hook可以是同步的,也可以是异步的。为了反映这一点,有“Sync”,“AsyncSeries”和“AsyncParallel” hook类:
Sync: Sync hook只能在同步函数中使用(使用myHook.tap())。
AsyncSeries: AsyncSeries hook可以通过同步、基于回调和基于承诺的函数(使用myHook.tap()、myHook.tapAsync()和myHook.tapPromise())来实现。它们依次船型调用每个异步方法
AsyncParallel: AsyncParallel hook也可以通过同步、基于回调和基于承诺的函数(使用myHook.tap()、myHook.tapAsync()和myHook.tapPromise())来实现。然而,它们并行地运行每个异步方法。
钩子类型反映在它的类名中。例如,AsyncSeriesWaterfallHook允许异步函数并串联运行它们,将每个函数的返回值传递给下一个函数。
拦截
所有钩子都提供了一个额外的拦截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; // 可能返回一个新的tapInfo对象
}
})
call: (…args) => 当钩子被触发时,将会触发对拦截器的调用。您可以访问hook参数。
tap: (tap: Tap) => 在你的拦截器中添加tap会在插件插入钩子时触发。提供的是tap对象。tap对象不能被改变。
loop: (…args) => 在你的拦截器中添加loop会触发一个循环钩子的每个循环。
register: (tap: Tap) => 为你的拦截器添加寄存器将会触发每次添加的tap,并允许修改它。
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: true`, `context`开始时为空对象。
// 如果没有插件使用' context: true ',那么' context '是未定义的。
if (context) {
// 任意属性都可以添加到“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是带有钩子的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
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,
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
}
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
一个类似钩子的helper类,重定向点击到多个其他钩子:
const { MultiHook } = require("tapable");
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);