[6a3a178] | 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 | ```
|
---|