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.

294 lines
8.4 KiB

4 years 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. setSpeed(newSpeed) {
  77. this.hooks.accelerate.call(newSpeed);
  78. }
  79. useNavigationSystemPromise(source, target) {
  80. const routesList = new List();
  81. return this.hooks.calculateRoutes.promise(source, target, routesList).then(() => {
  82. return routesList.getRoutes();
  83. });
  84. }
  85. useNavigationSystemAsync(source, target, callback) {
  86. const routesList = new List();
  87. this.hooks.calculateRoutes.callAsync(source, target, routesList, err => {
  88. if(err) return callback(err);
  89. callback(null, routesList.getRoutes());
  90. });
  91. }
  92. }
  93. ```
  94. The Hook will compile a method with the most efficient way of running your plugins. It generates code depending on:
  95. * The number of registered plugins (none, one, many)
  96. * The kind of registered plugins (sync, async, promise)
  97. * The used call method (sync, async, promise)
  98. * The number of arguments
  99. * Whether interception is used
  100. This ensures fastest possible execution.
  101. ## Hook types
  102. Each hook can be tapped with one or several functions. How they are executed depends on the hook type:
  103. * Basic hook (without “Waterfall”, “Bail” or “Loop” in its name). This hook simply calls every function it tapped in a row.
  104. * __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.
  105. * __Bail__. A bail hook allows exiting early. When any of the tapped function returns anything, the bail hook will stop executing the remaining ones.
  106. * __Loop__. TODO
  107. Additionally, hooks can be synchronous or asynchronous. To reflect this, there’re “Sync”, “AsyncSeries”, and “AsyncParallel” hook classes:
  108. * __Sync__. A sync hook can only be tapped with synchronous functions (using `myHook.tap()`).
  109. * __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.
  110. * __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.
  111. 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.
  112. ## Interception
  113. All Hooks offer an additional interception API:
  114. ``` js
  115. myCar.hooks.calculateRoutes.intercept({
  116. call: (source, target, routesList) => {
  117. console.log("Starting to calculate routes");
  118. },
  119. register: (tapInfo) => {
  120. // tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
  121. console.log(`${tapInfo.name} is doing its job`);
  122. return tapInfo; // may return a new tapInfo object
  123. }
  124. })
  125. ```
  126. **call**: `(...args) => void` Adding `call` to your interceptor will trigger when hooks are triggered. You have access to the hooks arguments.
  127. **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.
  128. **loop**: `(...args) => void` Adding `loop` to your interceptor will trigger for each loop of a looping hook.
  129. **register**: `(tap: Tap) => Tap | undefined` Adding `register` to your interceptor will trigger for each added `Tap` and allows to modify it.
  130. ## Context
  131. 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.
  132. ``` js
  133. myCar.hooks.accelerate.intercept({
  134. context: true,
  135. tap: (context, tapInfo) => {
  136. // tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
  137. console.log(`${tapInfo.name} is doing it's job`);
  138. // `context` starts as an empty object if at least one plugin uses `context: true`.
  139. // If no plugins use `context: true`, then `context` is undefined.
  140. if (context) {
  141. // Arbitrary properties can be added to `context`, which plugins can then access.
  142. context.hasMuffler = true;
  143. }
  144. }
  145. });
  146. myCar.hooks.accelerate.tap({
  147. name: "NoisePlugin",
  148. context: true
  149. }, (context, newSpeed) => {
  150. if (context && context.hasMuffler) {
  151. console.log("Silence...");
  152. } else {
  153. console.log("Vroom!");
  154. }
  155. });
  156. ```
  157. ## HookMap
  158. A HookMap is a helper class for a Map with Hooks
  159. ``` js
  160. const keyedHook = new HookMap(key => new SyncHook(["arg"]))
  161. ```
  162. ``` js
  163. keyedHook.tap("some-key", "MyPlugin", (arg) => { /* ... */ });
  164. keyedHook.tapAsync("some-key", "MyPlugin", (arg, callback) => { /* ... */ });
  165. keyedHook.tapPromise("some-key", "MyPlugin", (arg) => { /* ... */ });
  166. ```
  167. ``` js
  168. const hook = keyedHook.get("some-key");
  169. if(hook !== undefined) {
  170. hook.callAsync("arg", err => { /* ... */ });
  171. }
  172. ```
  173. ## Hook/HookMap interface
  174. Public:
  175. ``` ts
  176. interface Hook {
  177. tap: (name: string | Tap, fn: (context?, ...args) => Result) => void,
  178. tapAsync: (name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
  179. tapPromise: (name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
  180. intercept: (interceptor: HookInterceptor) => void
  181. }
  182. interface HookInterceptor {
  183. call: (context?, ...args) => void,
  184. loop: (context?, ...args) => void,
  185. tap: (context?, tap: Tap) => void,
  186. register: (tap: Tap) => Tap,
  187. context: boolean
  188. }
  189. interface HookMap {
  190. for: (key: any) => Hook,
  191. tap: (key: any, name: string | Tap, fn: (context?, ...args) => Result) => void,
  192. tapAsync: (key: any, name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
  193. tapPromise: (key: any, name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
  194. intercept: (interceptor: HookMapInterceptor) => void
  195. }
  196. interface HookMapInterceptor {
  197. factory: (key: any, hook: Hook) => Hook
  198. }
  199. interface Tap {
  200. name: string,
  201. type: string
  202. fn: Function,
  203. stage: number,
  204. context: boolean
  205. }
  206. ```
  207. Protected (only for the class containing the hook):
  208. ``` ts
  209. interface Hook {
  210. isUsed: () => boolean,
  211. call: (...args) => Result,
  212. promise: (...args) => Promise<Result>,
  213. callAsync: (...args, callback: (err, result: Result) => void) => void,
  214. }
  215. interface HookMap {
  216. get: (key: any) => Hook | undefined,
  217. for: (key: any) => Hook
  218. }
  219. ```
  220. ## MultiHook
  221. A helper Hook-like class to redirect taps to multiple other hooks:
  222. ``` js
  223. const { MultiHook } = require("tapable");
  224. this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
  225. ```