走进 Tapable

avatar
前端开发工程师 @广州

一、引子

webpack4 之后,编写 插件(Plugin) 的 "套路",类似这样:

class XXXWebpackPlugin {
  constructor (options) {
    // ...
  }

  apply (compiler) {
    // ...
    compiler.hooks.done.tap('CaseSensitivePathsPlugin', onDone);
    if (this.options.useBeforeEmitHook) {
      // ...
      compiler.hooks.emit.tapAsync(
        'CaseSensitivePathsPlugin',
        (compilation, callback) => {
          // ...
        },
      );
    } else {
      compiler.hooks.normalModuleFactory.tap(
        'CaseSensitivePathsPlugin',
        (arg) => {
          // ...
        },
      );
    }
   // ...
}

依葫芦画瓢,也可以写出自己想要的 Plugin,但总会好奇:taptapAsync是什么?为什么是这样的"套路"?好奇心驱使着你打破砂锅问到底,翻看源码找到 webpack 里的两个核心的对象:CompilerCompilation

class Compiler extends Tapable{
    // ...
}

class Compilation extends Tapable {
    // ...
}

发现它们都是继承自 一个叫 Tapable 的类……这是何方神圣?

二、什么是Tapable

翻看 Tapable 的官方 Readme 文档,上面并没有给出关于 Tapable 的明确定义,上来就是一堆 API 和 Demo,快速看一遍感觉云里雾里,说了又好像没完全说,这是只可意会不可言传的意思? confused.gif

通过查阅其他资料后,总算有了一个初步的概念: 简单的说,Tapable 就是一个实现了一种发布订阅模式的库。 它提供了一系列事件的发布订阅 API ,通过这些 API 我们可以注册事件,并在不同时机去触发该事件。 Publisher&Subscriber.png

是不是很熟悉,类似我们熟悉的生命周期钩子。是的,Tapable 提供了类似的东西,那些API 基本就是各种类型的钩子(Hook)。

那么接下来就走进 Tapable,看看它是不是如上所说那样,也看看它究竟提供了哪些能力?

三、Tapable 初体验

初来乍到,肯定得来点简单的先试试水,先把最简单的 SyncHook 摆上台面:

// SyncHook(同步钩子)是最基础的同步钩子:
const { SyncHook } = require("tapable");

// 初始化同步钩子
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

// 注册事件
hook.tap("Dest_1", (arg1, arg2, arg3) => {
    console.log("Dest_1:", arg1, arg2, arg3);
});

hook.tap("Dest_2", (arg1, arg2, arg3) => {
    console.log("Dest_2:", arg1, arg2, arg3);
});

// 调用事件并传递参数
hook.call("Voyager I", "Tech", "Team");

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * Dest_2: Voyager I Tech Team
 */

以上代码可以概括为如下几步:

  1. 第一步:创建Hook实例,通过 new 关键字对 Hook 类进行实例化。
    • new Hook 时接受一个 字符串数组 作为参数,数组中的值不重要,重要的是数组中对应的字符串个数,请先记住这点。
  2. 第二步:通过 tap 函数注册对应的事件,注册事件时接受两个参数:
    • 第一个参数是一个字符串,它没有任何实际意义仅仅是一个标识位而已。Tip: 这个参数还可以为一个对象,后续会提到

      注意:不要跟我们熟悉的“自定义事件”里的“事件名”搞混,这里它真的就是一个标识位而已,没多大作用,不用纠结。如果你没纠结,就当我什么都没说- -!

    • 第二个参数是本次注册事件的回调函数,在调用相应钩子时会执行这个函数。

      注意:它能携带多少个参数,取决于前面 new Hook 中 字符串数组中元素的个数。

  3. 最后就是我们通过 call 方法传入对应的参数,调用注册在 hook 内部的事件函数进行执行。
    • 同时在 call 方法执行时,会将 call 方法传入的参数传递给每一个注册的事件函数作为实参进行调用。

      注意:它能传递多少个参数,取决于前面 new Hook 中 字符串数组中元素的个数。

看着是不是很简单?这就是前面提到 webpack 插件代码套路的由来。当然,Tapable 提供的钩子(Hook) 不止于此,还有更多强大、有趣的钩子(Hook),我们都来会一会!

四、钩子(Hook)的分类

Tapable 官方 README 文档提供了这九种钩子:

SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook,
// 其实还有一个……
AsyncSeriesLoopHook // 源码中提供了,但官方 README 文档中没有提及

看它们名字,就感觉得出是某种排列组合拼凑出来的。是的,他们的名字正是基于钩子本身的特征命名的,根据这些特征,可以给以上钩子(Hook)分类:

  1. 按照同步(Sync)/异步(Async)分类:在 Tapable 中所有注册的事件可以分为同步、异步两种执行方式,正如名称表述的那样:
    • 同步表示注册的 事件函数 会同步进行执行。
    • 异步表示注册的 事件函数 会异步进行执行。 syncAsync.png 针对同步钩子来 tap 方法是唯一的注册事件的方法,通过 call 方法触发同步钩子的执行。异步钩子可以通过 taptapAsynctapPromise 三种方式来注册,同时可以通过对应的 callcallAsyncpromise( 注意这里的 promise 是 Tapable 钩子实例方法,不要跟 Promise API 搞混 ) 三种方式来触发注册的函数。

同时 异步钩子可以分为:

  • 异步串行钩子(AsyncSeries):可以串行(连续按照顺序调用)执行的异步钩子函数。
  • 异步并行钩子(AsyncParallel):可以并行(并发调用)执行的异步钩子函数。
  1. 按照执行机制分类
    • Basic Hook: 基本钩子。这个钩子只会【按顺序】连续调用每个注册的事件函数 basicHook.png
    • Waterfall: 瀑布钩子也会按顺序连续调用每个注册的 事件函数 。与基本钩子不同,它将【返回值】从一个 事件函数 传递到下一个 事件函数 作为参数。如果中间某个函数没有返回值,则将上一个存在的返回值传下去。另外,当下一个 事件函数 存在多个参数时,返回值仅能修改他的第一个参数 waterfallHook.png
    • Bail: 保险类型钩子,保险类型钩子在基础类型钩子上增加了一种保险机制,如果任意一个注册函数执行的返回值不是 undefined,那么整个钩子执行过程会立即中断,停止执行其余的 事件函数 BailHook.png
    • Loop: 循环类型钩子,当循环钩子中事件函数的返回值不是 undefined 时,钩子将从第一个插件重新启动。它将循环,直到所有 事件函数 返回未定义 LoopHook.png

五、亲密接触 Tapable

vr.gif

上述概念都不会太难理解,但是很多时候细节是魔鬼,“纸上得来终觉浅,绝知此事要躬行”……况且官网 Readme 文档又实在不太亲民,所以还是把各个钩子(Hook)通过简单的Demo过一遍,尽量做到能让各位看官耐心跟着过一遍之后,都会觉得:“嗐,就这?!”

同步钩子相对简单,最基本的 SyncHook 前面已经分析过,不再赘述;下面看看其他同步钩子的表现:

SyncBailHook

SyncBailHook(同步保险钩子) 中如果任何事件的回调函数的返回值不为undefined,那么会立即中断后续事件函数的调用:

const { SyncBailHook } = require("tapable");

const hook = new SyncBailHook(["arg1", "arg2", "arg3"]);

// 注册事件
hook.tap("Dest_1", (arg1, arg2, arg3) => {
    console.log("Dest_1:", arg1, arg2, arg3);
    // 返回值不为 undefined,则阻断Dest_2事件的调用
    return true;
});

hook.tap("Dest_2", (arg1, arg2, arg3) => {
    console.log("Dest_2:", arg1, arg2, arg3);
});

// 调用事件并传递执行参数
hook.call("Voyager I", "Tech", "Team");

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 */

SyncWaterfallHook

SyncWaterfallHook(同步瀑布钩子) 瀑布钩子会将上一个函数的返回值传递给下一个函数作为参数; 重申: 如果某个函数返回 undefined,则继续将上一个函数的返回值传下去; 重申: 当存在多个参数时,通过 SyncWaterfallHook 仅能修改第一个参数的返回值。

const { SyncWaterfallHook } = require("tapable");

const hook = new SyncWaterfallHook(["arg1", "arg2", "arg3"]);

// 注册事件
hook.tap("Dest_1", (arg1, arg2, arg3) => {
    console.log("Dest_1:", arg1, arg2, arg3);
    // 存在返回值 修改Dest_2函数的实参
    return "Voyager II";
});

hook.tap("Dest_2", (arg1, arg2, arg3) => {
    console.log("Dest_2:", arg1, arg2, arg3);
    return "Voyager III";
});

hook.tap("Dest_3", (arg1, arg2, arg3) => {
    console.log("Dest_3:", arg1, arg2, arg3);
    // 此回调函数返回 undefined, 则继续将上一个回调函数的返回值 Voyager III 传递下去
});

hook.tap("Dest_4", (arg1, arg2, arg3) => {
    console.log("Dest_4:", arg1, arg2, arg3);
});

// 调用事件并传递执行参数
hook.call("Voyager I", "Tech", "Team");

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * Dest_2: Voyager II Tech Team
 * Dest_3: Voyager III Tech Team
 * Dest_4: Voyager III Tech Team
 */

SyncLoopHook

SyncLoopHook(同步循环钩子) 任意一个被监听的函数的返回值不是 undefined 时,则从头开始执行:

const { SyncLoopHook } = require("tapable");

let Dest_1 = 2;
let Dest_2 = 1;

const hook = new SyncLoopHook(["arg1", "arg2", "arg3"]);

// 注册事件
hook.tap("Dest_1", (arg1, arg2, arg3) => {
    console.log("Dest_1, value = ", Dest_1);
    if (Dest_1 !== 3) {
      return Dest_1++;
    }
});

hook.tap("Dest_2", (arg1, arg2, arg3) => {
    console.log("Dest_2, value = ", Dest_2);
    if (Dest_2 !== 3) {
      return Dest_2++;
    }
});

// 调用事件并传递执行参数
hook.call("Voyager I", "Tech", "Team");

/**
 * 执行结果
 *
 * Dest_1, value =  2
 * Dest_1, value =  3
 * Dest_2, value =  1
 * Dest_1, value =  3
 * Dest_2, value =  2
 * Dest_1, value =  3
 * Dest_2, value =  3
 */

异步钩子相对复杂一些,可以通过taptapAsynctapPromise三种方式来注册,同时可以通过对应的 callcallAsyncpromise三种方式来触发注册的函数。关于异步钩子的触发方式,有几点要特别提一下:

  1. 通过 tapAsync 注册时,实参结尾额外接受一个 callback 参数,调用 callback 表示本次事件执行完毕。
  2. callback 的传参机制和 nodejs 的回调函数传参机制一致,即首个参数表示错误对象;也就是说,调用 callback 函数时,如果第一个参数不是 undefined,就表示本次执行出现错误,流程终止。后续参数和 nodejs 的回调函数参数同理,即第二个参数开始表示本次函数调用的返回值。
  3. Promise 同理,如果这个 Promise 返回的结果是 reject 状态,那么和 callback 传递错误参数同样效果,也会中断后续事件函数的执行。

AsyncSeriesHook

先来看看异步钩子中最简单的 AsyncSeriesHook(异步串行钩子):

const { AsyncSeriesHook } = require("tapable");

const hook = new AsyncSeriesHook(["arg1", "arg2", "arg3"]);

console.time("旅行耗时");

// 注册事件
hook.tapAsync("Dest_1", (arg1, arg2, arg3, callback) => {
    console.log("Dest_1:", arg1, arg2, arg3);
    setTimeout(() => {
      // 1s后调用callback表示 Dest_1执行完成
      callback(); // Tip: callback 只能传递第一个参数(Error)
    }, 1000);
});

hook.tapPromise("Dest_2", (arg1, arg2, arg3) => {
console.log("Dest_2:", arg1, arg2, arg3);
    // tapPromise返回Promise
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(`Done`);
      }, 1000);
    });
});

// 调用事件并传递执行参数
hook.callAsync("Voyager I", "Tech", "Team", (err, res) => {
    console.log("旅行完毕", err, res);
    console.timeEnd("旅行耗时");
});

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * Dest_2: Voyager I Tech Team
 * 旅行完毕 undefined undefined
 * 旅行耗时: 2012.348ms
 */

也可以通过 promise 的方式来触发事件函数:

hook
  .promise("Voyager I", "Tech", "Team")
  .then((res) => {
    // Tip: res is undefined for AsyncSeriesHook
    // 即此处无法接收 tapPromise 中 Promise 的 resolve 传递的参数;也无法接收 callAsync 中 callback(undefined, xxx) 传递的第二个参数 xxx,与 callback 只能传递第一个参数(Error)呼应上了
    console.log("旅行完毕", res); 
    console.timeEnd("旅行耗时");
  })
  .catch((err) => {
    console.log("旅行失败", err);
    console.timeEnd("旅行耗时");
  });

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * Dest_2: Voyager I Tech Team
 * 旅行完毕 undefined
 * 旅行耗时: 2012.962ms
 */

如果前面的 callback 首个参数不为 undefined,相当于抛出了错误,此时 Dest_2 不会被执行:

// 注册事件
hook.tapAsync("Dest_1", (arg1, arg2, arg3, callback) => {
  console.log("Dest_1:", arg1, arg2, arg3);
  setTimeout(() => {
    callback(true);
  }, 1000);
});

hook.tapPromise("Dest_2", (arg1, arg2, arg3) => {
  console.log("Dest_2:", arg1, arg2, arg3);
  // tapPromise返回Promise
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`Done`);
    }, 1000);
  });
});

// 调用事件并传递执行参数
hook.callAsync("Voyager I", "Tech", "Team", (err, res) => {
  console.log("旅行完毕", err, res);
  console.timeEnd("旅行耗时");
});

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * 旅行完毕 true undefined
 * 旅行耗时: 1011.078ms
 */

如果通过 promise 的方式来触发事件函数,错误被 catch 捕获:

// 注册事件
hook.tapAsync("Dest_1", (arg1, arg2, arg3, callback) => {
  console.log("Dest_1:", arg1, arg2, arg3);
  setTimeout(() => {
    callback(true);
  }, 1000);
});

hook.tapPromise("Dest_2", (arg1, arg2, arg3) => {
  console.log("Dest_2:", arg1, arg2, arg3);
  // tapPromise返回Promise
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`Done`);
    }, 1000);
  });
});

// 调用事件并传递执行参数(promise方式)
hook
  .promise("Voyager I", "Tech", "Team")
  .then((res) => {
    console.log("旅行完毕", res);
    console.timeEnd("旅行耗时");
  })
  .catch((err) => {
    console.log("旅行失败", err);
    console.timeEnd("旅行耗时");
  });

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * 旅行失败 true
 * 旅行耗时: 1014.715ms
 */

AsyncSeriesBailHook

异步串行保险钩子,跟同步保险钩子类似,区别仅在于注册的事件函数是异步函数:

const { AsyncSeriesBailHook } = require("tapable");
const hook = new AsyncSeriesBailHook(["arg1", "arg2", "arg3"]);

console.time("旅行耗时");

// 注册事件
hook.tapPromise("Dest_1", (arg1, arg2, arg3, callback) => {
  console.log("Dest_1:", arg1, arg2, arg3);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 如果resolve函数传入的值不为undefined,则中断后续函数的执行
      resolve();
    }, 1000);
  });
});

hook.tapAsync("Dest_2", (arg1, arg2, arg3, callback) => {
  console.log("Dest_2:", arg1, arg2, arg3);
  setTimeout(() => {
    // callback 的传参机制和 nodejs 中的回调函数一致
    callback(undefined, true); // 表示返回值不为 undefined,中断后续函数的执行

    // 特别提示:
    // 如果callback的第一个参数不为 undefined,例如`callback(true)`,此时表示抛出错误,这个时候也会中断后续流程
    // 如果是由promise方式触发的,将会由catch捕获此错误
  }, 1000);
});

// Dest_3的事件函数不会被执行
hook.tapAsync("Dest_3", (arg1, arg2, arg3, callback) => {
  console.log("Dest_3:", arg1, arg2, arg3);
  setTimeout(() => {
    callback();
  }, 1000);
});

// 调用事件并传递执行参数
hook.callAsync("Voyager I", "Tech", "Team", (err, res) => {
  console.log("旅行完毕", err, res);
  console.timeEnd("旅行耗时");
});

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * Dest_2: Voyager I Tech Team
 * 旅行完毕 null true
 * 旅行耗时: 2013.494ms
 */

AsyncSeriesWaterfallHook

异步串行瀑布钩子,跟同步瀑布钩子类似,区别仅在于注册的事件函数是异步函数:

const { AsyncSeriesWaterfallHook } = require("tapable");
const hook = new AsyncSeriesWaterfallHook(["arg1", "arg2", "arg3"]);

console.time("旅行耗时");

// 注册事件
hook.tapPromise("Dest_1", (arg1, arg2, arg3) => {
  console.log("Dest_1:", arg1, arg2, arg3);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Voyager II`);
    }, 1000);
  });
});

hook.tapAsync("Dest_2", (arg1, arg2, arg3, callback) => {
  console.log("Dest_2:", arg1, arg2, arg3);
  setTimeout(() => {
    callback();
  }, 1000);
});

// 调用事件并传递执行参数;
hook.callAsync("Voyager I", "Tech", "Team", (err, res) => {
  console.log("旅行完毕", err, res);
  console.timeEnd("旅行耗时");
});

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * Dest_2: Voyager II Tech Team
 * 旅行完毕 null Voyager II // Tip: Voyager II 为 resolve(`Voyager II`) 传递的参数
 * 旅行耗时: 2014.139ms
 */

AsyncParallelHook

异步并行钩子,会并发执行所有异步钩子:

const { AsyncParallelHook } = require("tapable");
const hook = new AsyncParallelHook(["arg1", "arg2", "arg3"]);

console.time("旅行耗时");

// 注册事件
hook.tapPromise("Dest_1", (arg1, arg2, arg3) => {
  console.log("Dest_1:", arg1, arg2, arg3);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Dest_1"); // Tip: 无法通过 resolve 传递参数...
    }, 1000);
  });
});

hook.tapAsync("Dest_2", (arg1, arg2, arg3, callback) => {
  console.log("Dest_2:", arg1, arg2, arg3);
  setTimeout(() => {
    // Tip: callback 只能传递第一个参数(Error)
    callback(undefined, `Dest_2`);
  }, 1000);
});

// 调用事件并传递执行参数
hook.callAsync("Voyager I", "Tech", "Team", (err, res) => {
  console.log("旅行完毕", err, res);
  console.timeEnd("旅行耗时");
});

/**
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * Dest_2: Voyager I Tech Team
 * 旅行完毕 undefined undefined
 * 旅行耗时: 1011.015ms  // 两个事件函数并行开始执行,在1s后两个异步函数执行结束,整体回调结束。
 */

AsyncParallelBailHook

异步并行保险钩子,这个钩子有点特别:

const { AsyncParallelBailHook } = require("tapable");
const hook = new AsyncParallelBailHook(["arg1", "arg2", "arg3"]);

console.time("旅行耗时");

// 注册事件
hook.tapPromise("Dest_1", (arg1, arg2, arg3) => {
  return new Promise((resolve, reject) => {
    console.log("Dest_1 done:", arg1, arg2, arg3);
    setTimeout(() => {
      resolve(true); // resolve(true) 返回了非 undefined 的值,此时 hook 会发生保险效果,停止后续所有的事件函数调用。
    }, 1000);
  });
});

hook.tapAsync("Dest_2", (arg1, arg2, arg3, callback) => {
  setTimeout(() => {
    console.log("Dest_2 done:", arg1, arg2, arg3);
    callback();
  }, 3000);
});

// 调用事件并传递执行参数
hook.callAsync("Voyager I", "Tech", "Team", (err, res) => {
  console.log("旅行完毕", err, res);
  console.timeEnd("旅行耗时");
});

/**
 * 执行结果
 *
 * Dest_1 done: Voyager I Tech Team
 * 旅行完毕 null true
 * 旅行耗时: 1014.579ms
 * // 此时表示hook执行完毕的callback已经执行完毕了
 *
 * // 由于是异步并行的原因,所以在最开始所有的事件函数都会被并行执行,所以3s后会执行定时器的打印
 * Dest_2 done: Voyager I Tech Team 
 */

心里犯嘀咕:Dest_2 还是执行了,并没有被中断,这样还能叫保险钩子吗?是使用的姿势不对?

AsyncSeriesLoopHook

AsyncSeriesLoopHook(异步串行循环钩子),跟同步循环钩子类似,区别仅在于注册的事件函数是异步函数:

const { AsyncSeriesLoopHook } = require("tapable");
const hook = new AsyncSeriesLoopHook(["arg1", "arg2", "arg3"]);

console.time("旅行耗时");

let Dest_1 = 2;
let Dest_2 = 2;

// 注册事件
hook.tapPromise("Dest_1", (arg1, arg2, arg3) => {
  console.log("Dest_1, value = ", Dest_1);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Dest_1 !== 3) {
        resolve(Dest_1++);
      } else {
        resolve();
      }
    }, 1000);
  });
});

hook.tapAsync("Dest_2", (arg1, arg2, arg3, callback) => {
  console.log("Dest_2, value = ", Dest_2);
  setTimeout(() => {
    if (Dest_2 !== 3) {
      callback(undefined, Dest_2++);
    } else {
      callback();
    }
  }, 1000);
});

// callAsync;
hook.callAsync("Voyager I", "Tech", "Team", (err, res) => {
  console.log("旅行完毕", err, res); // Tip: 不可能收得到 非undefined的res... 因为每传一个 非undefined的res,就会重头开始执行...
  console.timeEnd("旅行耗时");
});

/**
 * 执行结果
 *
 * Dest_1, value =  2
 * Dest_1, value =  3
 * Dest_2, value =  2
 * Dest_1, value =  3
 * Dest_2, value =  3
 * 旅行完毕 undefined undefined
 * 旅行耗时: 5027.637ms
 */

至此,所有钩子(Hook)都拉出来体验了一遍(不容易吖- -!),是不是感觉“也就这”。然鹅还没结束…… more.gif

Tapable 还提供了一些其他API,可以让使用者更灵活地监控钩子(Hook)的全过程。感兴趣的朋友可以继续往下看。

六、拦截器

所有钩子都提供了一个额外的拦截器API - intercept,它和 Axios 中的拦截器的效果非常类似。我们可以通过拦截器对整个 Tapable 发布/订阅流程进行监听,从而触发对应的逻辑。

const { SyncHook } = require("tapable");
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

hook.intercept({
  // 每次调用 hook 实例的 tap() 方法注册回调函数时, 都会调用该方法, 并且接受 tap 作为参数, 还可以对 tap 进行修改;
  register: (tapInfo) => {
    console.log(tapInfo);
    console.log(`${tapInfo.name} is doing its job`); // tapInfo.name就是注册事件时传入的标识符...
    return tapInfo; // may return a new tapInfo object
  },
  // 通过 hook实例对象 上的call方法时候触发拦截器
  call: (arg1, arg2, arg3) => {
    console.log("Starting to call", arg1, arg2, arg3);
  },
  // 在调用被注册的每一个事件函数之前执行
  tap: (tapInfo) => {
    console.log(tapInfo, "Starting to tap");
  },
  // loop类型 钩子中 每个事件函数被调用前触发该拦截器方法
  loop: (...args) => {
    console.log(args, "loop");
  },
});

// 注册事件
hook.tap("Dest_1", (arg1, arg2, arg3) => {
  console.log("Dest_1 call:", arg1, arg2, arg3);
});

hook.tap("Dest_2", (arg1, arg2, arg3) => {
  console.log("Dest_2 call:", arg1, arg2, arg3);
});

// 调用事件并传递执行参数
hook.call("Voyager I", "Tech", "Team");

/**
 * 执行结果
 *
 * { type: 'sync', fn: [Function], name: 'Dest_1' } // tapInfo
 * Dest_1 is doing its job
 * { type: 'sync', fn: [Function], name: 'Dest_2' }
 * Dest_2 is doing its job
 * Starting to call Voyager I Tech Team
 * { type: 'sync', fn: [Function], name: 'Dest_1' } Starting to tap
 * Dest_1 call: Voyager I Tech Team
 * { type: 'sync', fn: [Function], name: 'Dest_2' } Starting to tap
 * Dest_2 call: Voyager I Tech Team
 */
  • call: 钩子被触发时(call, callAsync, promise)执行,接受的参数为调用钩子时传入的参数。
  • tap: 在每一个被注册的事件函数调用之前执行,接受的参数为对应的 Tap 对象。
  • loop: 循环类型钩子中 每次重新开始 loop 之前执行,接受的参数为调用时传入的参数。
  • register: 每次通过 taptapAsynctapPromise 方法注册事件函数时,会触发该拦截器函数。这个拦截器中接受注册的 Tap 作为参数,同时可以对于注册的事件进行修改。
  • context: 上下文,后文再详述到其作用(即将废弃)

七、Before 和 Stage 属性

前面提过,在注册事件函数时,第一个参数支持传入一个对象。 我们可以通过这个对象上的 stage 和 before 属性来控制本次注册的事件函数执行时机。

Before 属性

Before 属性的值可以传入一个数组或者字符串,值为注册事件对象时的名称,它可以修改当前事件函数在传入的事件名称对应的函数之前进行执行。

const { SyncHook } = require("tapable");

const hooks = new SyncHook();

hooks.tap({ name: "Dest_1" }, () => console.log("Dest_1 Done."));

hooks.tap(
  {
    name: "Dest_2",
    // Tip: Dest_2 事件函数会在Dest_1之前进行执行
    before: "Dest_1",
  },
  () => console.log("Dest_2 Done.")
);

hooks.call();
/**
 * 执行结果
 *
 * Dest_2 Done.
 * Dest_1 Done.
 */

Stage 属性

Stage 属性的类型是数字,数字越大,事件回调执行的越晚,支持传入负数,默认为0.

const hooks2 = new SyncHook();

hooks2.tap(
  {
    name: "Dest_1",
    stage: 1,
  },
  () => console.log("Dest_1 Done.")
);

hooks2.tap(
  {
    name: "Dest_2",
    // stage默认为0,
  },
  () => console.log("Dest_2 Done.")
);

hooks2.call();
/**
 * 执行结果
 *
 * Dest_2 Done.
 * Dest_1 Done.
 */

如果同时使用 before 和 stage 时,优先会处理 before ,在满足 before 的条件之后才会进行 stage 的判断。before 和 stage 都可以修改事件回调函数的执行时间,但是不建议同时配置这两个属性。

八、HookMap

HookMap 本质上就是一个辅助类,通过 HookMap 我们可以更好的管理 Hook :

const { HookMap, SyncHook } = require("tapable");

// 创建HookMap实例
const keyedHook = new HookMap((key) => new SyncHook(["arg"]));

// 在 keyedHook 中创建一个 name 为 VoyagerI 的 hook,同时为该 hook 通过 tap 注册事件
keyedHook.for("VoyagerI").tap("Dest_1", (arg) => {
  console.log(arg, "Voyager_1");
});

// 在 keyedHook 中创建一个 name 为 VoyagerII 的 hook,同时为该 hook 通过 tap 注册事件
keyedHook.for("VoyagerII").tap("Dest_2", (arg) => {
  console.log(arg, "Voyager_2");
});

// 在 keyedHook 中创建一个 name 为 VoyagerIII 的 hook,同时为该 hook 通过 tap 注册事件
keyedHook.for("VoyagerIII").tap("Dest_3", (arg) => {
  console.log(arg, "Voyager_3");
});

// 从 HookMap 中拿到 name 为 VoyagerI 的 hook
const hook = keyedHook.get("VoyagerI");

if (hook) {
  // 通过 call方法 触发Hook
  hook.call("hello");
}
/**
 * 执行结果
 *
 * hello Voyager_1
 */

九、MultiHook

在日常应用中并不是很常见,它的主要作用也就是通过 MultiHook 批量注册事件函数在多个钩子中。

const { SyncHook, SyncBailHook, SyncWaterfallHook, MultiHook } = require("tapable");

const hook = new SyncHook(["arg1", "arg2", "arg3"]);
const bhook = new SyncBailHook(["arg1", "arg2", "arg3"]);
const whook = new SyncWaterfallHook(["arg1", "arg2", "arg3"]);

// 在多个钩子中批量注册同一个事件函数
const allHooks = new MultiHook([hook, bhook, whook]);
allHooks.tap("Dest_1", (arg1, arg2, arg3) => {
  console.log("Dest_1:", arg1, arg2, arg3);
});

hook.call("Voyager I", "Tech", "Hook");
bhook.call("Voyager I", "Tech", "Bail Team");
whook.call("Voyager I", "Tech", "Waterfall Team");
/***
 * 执行结果
 *
 * Dest_1: Voyager I Tech Team
 * Dest_1: Voyager I Tech Bail Team
 * Dest_1: Voyager I Tech Waterfall Team
 */

十、Context

Context(上下文),顾名思义,就是提供给各事件函数的上下文。注册事件函数和拦截器中 可以选择开启 context 来访问一个可选对象 context,该对象可用于将任意值传递给后续 注册事件函数和拦截器。

const { SyncHook } = require("tapable");

const hook = new SyncHook(["arg1"]);

hook.intercept({
  context: true,
  // 在调用被注册的每一个事件函数之前执行
  tap: (context, tapInfo) => {
    // tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
    console.log(`${tapInfo.name} is doing it's job`);

    // 如果存在 注册事件 使用了`context:true`,则`context`初始化为空对象,否则`context`为 undefined。
    if (context) {
      // 可以将任意属性添加到`context`中,然后插件可以访问这些属性。
      context.hasMuffler = true;
    }
  },
});

// 此处注册事件的第一个参数就传入了一个对象
hook.tap(
  {
    name: "VoyagerI",
    context: true,
  },
  (context, arg1) => {
    if (context && context.hasMuffler) {
      console.log("Silence...");
    } else {
      console.log("Vroom!", arg1);
    }
  }
);

hook.call("Done");
/**
 * 执行结果
 *
 * VoyagerI is doing it's job
 * Silence...
 * (node:5384) DeprecationWarning: Hook.context is deprecated and will be removed --- 提示 Hook.context 即将废弃
 */

写在最后

如果有兴趣了解 Webpack Plugin 的话,那么 Tapable 是你必须要掌握的前置知识。 通过以上沉浸式的体验,相信各位对 Tapable 可以有很清晰的理解,再去回看 webpack 的插件、源码啥的,对 taptapAsynctapPromise 这些套路,应该可以会心一笑了。 立个flag,下一次,再深入研究一下 Tapable 的原理,期待下次再会~!

参考