You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

296 lines
8.5 KiB

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