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 | ```
|
---|