UNPKG

8.74 kBMarkdownView Raw
1# Tapable
2
3The tapable package expose many Hook classes, which can be used to create hooks for plugins.
4
5``` javascript
6const {
7 SyncHook,
8 SyncBailHook,
9 SyncWaterfallHook,
10 SyncLoopHook,
11 AsyncParallelHook,
12 AsyncParallelBailHook,
13 AsyncSeriesHook,
14 AsyncSeriesBailHook,
15 AsyncSeriesWaterfallHook
16 } = require("tapable");
17```
18
19## Installation
20
21``` shell
22npm install --save tapable
23```
24
25## Usage
26
27All Hook constructors take one optional argument, which is a list of argument names as strings.
28
29``` js
30const hook = new SyncHook(["arg1", "arg2", "arg3"]);
31```
32
33The best practice is to expose all hooks of a class in a `hooks` property:
34
35``` js
36class Car {
37 constructor() {
38 this.hooks = {
39 accelerate: new SyncHook(["newSpeed"]),
40 brake: new SyncHook(),
41 calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
42 };
43 }
44
45 /* ... */
46}
47```
48
49Other people can now use these hooks:
50
51``` js
52const myCar = new Car();
53
54// Use the tap method to add a consument
55myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
56```
57
58It's required to pass a name to identify the plugin/reason.
59
60You may receive arguments:
61
62``` js
63myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
64```
65
66For sync hooks, `tap` is the only valid method to add a plugin. Async hooks also support async plugins:
67
68``` js
69myCar.hooks.calculateRoutes.tapPromise("GoogleMapsPlugin", (source, target, routesList) => {
70 // return a promise
71 return google.maps.findRoute(source, target).then(route => {
72 routesList.add(route);
73 });
74});
75myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin", (source, target, routesList, callback) => {
76 bing.findRoute(source, target, (err, route) => {
77 if(err) return callback(err);
78 routesList.add(route);
79 // call the callback
80 callback();
81 });
82});
83
84// You can still use sync plugins
85myCar.hooks.calculateRoutes.tap("CachedRoutesPlugin", (source, target, routesList) => {
86 const cachedRoute = cache.get(source, target);
87 if(cachedRoute)
88 routesList.add(cachedRoute);
89})
90```
91The class declaring these hooks need to call them:
92
93``` js
94class Car {
95 /**
96 * You won't get returned value from SyncHook or AsyncParallelHook,
97 * to do that, use SyncWaterfallHook and AsyncSeriesWaterfallHook respectively
98 **/
99
100 setSpeed(newSpeed) {
101 // following call returns undefined even when you returned values
102 this.hooks.accelerate.call(newSpeed);
103 }
104
105 useNavigationSystemPromise(source, target) {
106 const routesList = new List();
107 return this.hooks.calculateRoutes.promise(source, target, routesList).then((res) => {
108 // res is undefined for AsyncParallelHook
109 return routesList.getRoutes();
110 });
111 }
112
113 useNavigationSystemAsync(source, target, callback) {
114 const routesList = new List();
115 this.hooks.calculateRoutes.callAsync(source, target, routesList, err => {
116 if(err) return callback(err);
117 callback(null, routesList.getRoutes());
118 });
119 }
120}
121```
122
123The Hook will compile a method with the most efficient way of running your plugins. It generates code depending on:
124* The number of registered plugins (none, one, many)
125* The kind of registered plugins (sync, async, promise)
126* The used call method (sync, async, promise)
127* The number of arguments
128* Whether interception is used
129
130This ensures fastest possible execution.
131
132## Hook types
133
134Each hook can be tapped with one or several functions. How they are executed depends on the hook type:
135
136* Basic hook (without “Waterfall”, “Bail” or “Loop” in its name). This hook simply calls every function it tapped in a row.
137
138* __Waterfall__. A waterfall hook also calls each tapped function in a row. Unlike the basic hook, it passes a return value from each function to the next function.
139
140* __Bail__. A bail hook allows exiting early. When any of the tapped function returns anything, the bail hook will stop executing the remaining ones.
141
142* __Loop__. When a plugin in a loop hook returns a non-undefined value the hook will restart from the first plugin. It will loop until all plugins return undefined.
143
144Additionally, hooks can be synchronous or asynchronous. To reflect this, there’re “Sync”, “AsyncSeries”, and “AsyncParallel” hook classes:
145
146* __Sync__. A sync hook can only be tapped with synchronous functions (using `myHook.tap()`).
147
148* __AsyncSeries__. An async-series hook can be tapped with synchronous, callback-based and promise-based functions (using `myHook.tap()`, `myHook.tapAsync()` and `myHook.tapPromise()`). They call each async method in a row.
149
150* __AsyncParallel__. An async-parallel hook can also be tapped with synchronous, callback-based and promise-based functions (using `myHook.tap()`, `myHook.tapAsync()` and `myHook.tapPromise()`). However, they run each async method in parallel.
151
152The hook type is reflected in its class name. E.g., `AsyncSeriesWaterfallHook` allows asynchronous functions and runs them in series, passing each function’s return value into the next function.
153
154
155## Interception
156
157All Hooks offer an additional interception API:
158
159``` js
160myCar.hooks.calculateRoutes.intercept({
161 call: (source, target, routesList) => {
162 console.log("Starting to calculate routes");
163 },
164 register: (tapInfo) => {
165 // tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
166 console.log(`${tapInfo.name} is doing its job`);
167 return tapInfo; // may return a new tapInfo object
168 }
169})
170```
171
172**call**: `(...args) => void` Adding `call` to your interceptor will trigger when hooks are triggered. You have access to the hooks arguments.
173
174**tap**: `(tap: Tap) => void` Adding `tap` to your interceptor will trigger when a plugin taps into a hook. Provided is the `Tap` object. `Tap` object can't be changed.
175
176**loop**: `(...args) => void` Adding `loop` to your interceptor will trigger for each loop of a looping hook.
177
178**register**: `(tap: Tap) => Tap | undefined` Adding `register` to your interceptor will trigger for each added `Tap` and allows to modify it.
179
180## Context
181
182Plugins and interceptors can opt-in to access an optional `context` object, which can be used to pass arbitrary values to subsequent plugins and interceptors.
183
184``` js
185myCar.hooks.accelerate.intercept({
186 context: true,
187 tap: (context, tapInfo) => {
188 // tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
189 console.log(`${tapInfo.name} is doing it's job`);
190
191 // `context` starts as an empty object if at least one plugin uses `context: true`.
192 // If no plugins use `context: true`, then `context` is undefined.
193 if (context) {
194 // Arbitrary properties can be added to `context`, which plugins can then access.
195 context.hasMuffler = true;
196 }
197 }
198});
199
200myCar.hooks.accelerate.tap({
201 name: "NoisePlugin",
202 context: true
203}, (context, newSpeed) => {
204 if (context && context.hasMuffler) {
205 console.log("Silence...");
206 } else {
207 console.log("Vroom!");
208 }
209});
210```
211
212## HookMap
213
214A HookMap is a helper class for a Map with Hooks
215
216``` js
217const keyedHook = new HookMap(key => new SyncHook(["arg"]))
218```
219
220``` js
221keyedHook.for("some-key").tap("MyPlugin", (arg) => { /* ... */ });
222keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ });
223keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => { /* ... */ });
224```
225
226``` js
227const hook = keyedHook.get("some-key");
228if(hook !== undefined) {
229 hook.callAsync("arg", err => { /* ... */ });
230}
231```
232
233## Hook/HookMap interface
234
235Public:
236
237``` ts
238interface Hook {
239 tap: (name: string | Tap, fn: (context?, ...args) => Result) => void,
240 tapAsync: (name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
241 tapPromise: (name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
242 intercept: (interceptor: HookInterceptor) => void
243}
244
245interface HookInterceptor {
246 call: (context?, ...args) => void,
247 loop: (context?, ...args) => void,
248 tap: (context?, tap: Tap) => void,
249 register: (tap: Tap) => Tap,
250 context: boolean
251}
252
253interface HookMap {
254 for: (key: any) => Hook,
255 intercept: (interceptor: HookMapInterceptor) => void
256}
257
258interface HookMapInterceptor {
259 factory: (key: any, hook: Hook) => Hook
260}
261
262interface Tap {
263 name: string,
264 type: string
265 fn: Function,
266 stage: number,
267 context: boolean,
268 before?: string | Array
269}
270```
271
272Protected (only for the class containing the hook):
273
274``` ts
275interface Hook {
276 isUsed: () => boolean,
277 call: (...args) => Result,
278 promise: (...args) => Promise<Result>,
279 callAsync: (...args, callback: (err, result: Result) => void) => void,
280}
281
282interface HookMap {
283 get: (key: any) => Hook | undefined,
284 for: (key: any) => Hook
285}
286```
287
288## MultiHook
289
290A helper Hook-like class to redirect taps to multiple other hooks:
291
292``` js
293const { MultiHook } = require("tapable");
294
295this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
296```