1 | # Tapable
|
2 |
|
3 | The tapable package expose many Hook classes, which can be used to create hooks for plugins.
|
4 |
|
5 | ``` javascript
|
6 | const {
|
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
|
22 | npm install --save tapable
|
23 | ```
|
24 |
|
25 | ## Usage
|
26 |
|
27 | All Hook constructors take one optional argument, which is a list of argument names as strings.
|
28 |
|
29 | ``` js
|
30 | const hook = new SyncHook(["arg1", "arg2", "arg3"]);
|
31 | ```
|
32 |
|
33 | The best practice is to expose all hooks of a class in a `hooks` property:
|
34 |
|
35 | ``` js
|
36 | class 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 |
|
49 | Other people can now use these hooks:
|
50 |
|
51 | ``` js
|
52 | const myCar = new Car();
|
53 |
|
54 | // Use the tap method to add a consument
|
55 | myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
|
56 | ```
|
57 |
|
58 | It's required to pass a name to identify the plugin/reason.
|
59 |
|
60 | You may receive arguments:
|
61 |
|
62 | ``` js
|
63 | myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
|
64 | ```
|
65 |
|
66 | For sync hooks, `tap` is the only valid method to add a plugin. Async hooks also support async plugins:
|
67 |
|
68 | ``` js
|
69 | myCar.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 | });
|
75 | myCar.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
|
85 | myCar.hooks.calculateRoutes.tap("CachedRoutesPlugin", (source, target, routesList) => {
|
86 | const cachedRoute = cache.get(source, target);
|
87 | if(cachedRoute)
|
88 | routesList.add(cachedRoute);
|
89 | })
|
90 | ```
|
91 | The class declaring these hooks need to call them:
|
92 |
|
93 | ``` js
|
94 | class 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 |
|
123 | The 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 |
|
130 | This ensures fastest possible execution.
|
131 |
|
132 | ## Hook types
|
133 |
|
134 | Each 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 |
|
144 | Additionally, 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 |
|
152 | The 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 |
|
157 | All Hooks offer an additional interception API:
|
158 |
|
159 | ``` js
|
160 | myCar.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 |
|
182 | Plugins 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
|
185 | myCar.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 |
|
200 | myCar.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 |
|
214 | A HookMap is a helper class for a Map with Hooks
|
215 |
|
216 | ``` js
|
217 | const keyedHook = new HookMap(key => new SyncHook(["arg"]))
|
218 | ```
|
219 |
|
220 | ``` js
|
221 | keyedHook.for("some-key").tap("MyPlugin", (arg) => { /* ... */ });
|
222 | keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ });
|
223 | keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => { /* ... */ });
|
224 | ```
|
225 |
|
226 | ``` js
|
227 | const hook = keyedHook.get("some-key");
|
228 | if(hook !== undefined) {
|
229 | hook.callAsync("arg", err => { /* ... */ });
|
230 | }
|
231 | ```
|
232 |
|
233 | ## Hook/HookMap interface
|
234 |
|
235 | Public:
|
236 |
|
237 | ``` ts
|
238 | interface 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 |
|
245 | interface 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 |
|
253 | interface HookMap {
|
254 | for: (key: any) => Hook,
|
255 | intercept: (interceptor: HookMapInterceptor) => void
|
256 | }
|
257 |
|
258 | interface HookMapInterceptor {
|
259 | factory: (key: any, hook: Hook) => Hook
|
260 | }
|
261 |
|
262 | interface Tap {
|
263 | name: string,
|
264 | type: string
|
265 | fn: Function,
|
266 | stage: number,
|
267 | context: boolean,
|
268 | before?: string | Array
|
269 | }
|
270 | ```
|
271 |
|
272 | Protected (only for the class containing the hook):
|
273 |
|
274 | ``` ts
|
275 | interface Hook {
|
276 | isUsed: () => boolean,
|
277 | call: (...args) => Result,
|
278 | promise: (...args) => Promise<Result>,
|
279 | callAsync: (...args, callback: (err, result: Result) => void) => void,
|
280 | }
|
281 |
|
282 | interface HookMap {
|
283 | get: (key: any) => Hook | undefined,
|
284 | for: (key: any) => Hook
|
285 | }
|
286 | ```
|
287 |
|
288 | ## MultiHook
|
289 |
|
290 | A helper Hook-like class to redirect taps to multiple other hooks:
|
291 |
|
292 | ``` js
|
293 | const { MultiHook } = require("tapable");
|
294 |
|
295 | this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
|
296 | ```
|