tapable

中文文档
2.3.3发布于 2026/04/21MITTypeScriptCommonJS零依赖构建来源已验证近 30 天下载 6.3亿

只是一个插件的小模块。

中文使用文档
文档目录27 节
  1. 安装
  2. 用法
  3. 插件API
  4. hook.withOptions(options)
  5. 钩类型
  6. 钩类
  7. SyncHook
  8. SyncBailHook
  9. SyncWaterfallHook
  10. SyncLoopHook
  11. AsyncParallelHook
  12. AsyncParallelBailHook
  13. AsyncSeriesHook
  14. AsyncSeriesBailHook
  15. AsyncSeriesLoopHook
  16. AsyncSeriesWaterfallHook
  17. 拦截
  18. 上下文
  19. HookMap
  20. 代码生成
  21. 为什么这很重要?
  22. 钩/ HookMap接口
  23. MultiHook
  24. 把一个龙头向几个钩子扇形展开
  25. 转发异步轻接
  26. 共享拦截器和选项
  27. isUsed

Tapable

tapable包公开了许多Hook类,这些类可用于为插件创建钩子。

const {
    AsyncParallelBailHook,
    AsyncParallelHook,
    AsyncSeriesBailHook,
    AsyncSeriesHook,
    AsyncSeriesWaterfallHook,
    SyncBailHook,
    SyncHook,
    SyncLoopHook,
    SyncWaterfallHook
} = require("tapable");

安装

npm install --save tapable

用法

所有Hook构造函数都有一个可选参数,它是一个字符串形式的参数名称列表。

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

最佳实践是在类中公开类的所有钩子 hooks 属性:

class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncHook(["newSpeed"]),
            brake: new SyncHook(),
            calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
        };
    }

    /* ... */
}

其他人现在可以使用这些钩子:

const myCar = new Car();

// Use the tap method to add a consumer (plugin)
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

它需要传递一个名称来标识插件/原因。

您可能会收到以下参数:

myCar.hooks.accelerate.tap("LoggerPlugin", (newSpeed) =>
    console.log(`Accelerating to ${newSpeed}`)
);

对于同步钩子, tap 是添加插件的唯一有效方法。异步钩子也支持异步插件:

myCar.hooks.calculateRoutes.tapPromise(
    "GoogleMapsPlugin",
    (source, target, routesList) =>
        // return a promise
        google.maps.findRoute(source, target).then((route) => {
            routesList.add(route);
        })
);
myCar.hooks.calculateRoutes.tapAsync(
    "BingMapsPlugin",
    (source, target, routesList, callback) => {
        bing.findRoute(source, target, (err, route) => {
            if (err) return callback(err);
            routesList.add(route);
            // call the callback
            callback();
        });
    }
);

// You can still use sync plugins
myCar.hooks.calculateRoutes.tap(
    "CachedRoutesPlugin",
    (source, target, routesList) => {
        const cachedRoute = cache.get(source, target);
        if (cachedRoute) routesList.add(cachedRoute);
    }
);

声明这些钩子的类需要调用它们:

class Car {
    /**
     * You won't get returned value from SyncHook or AsyncParallelHook,
     * to do that, use SyncWaterfallHook and AsyncSeriesWaterfallHook respectively
     */

    setSpeed(newSpeed) {
        // following call returns undefined even when you returned values
        this.hooks.accelerate.call(newSpeed);
    }

    useNavigationSystemPromise(source, target) {
        const routesList = new List();
        return this.hooks.calculateRoutes
            .promise(source, target, routesList)
            .then((res) =>
                // res is undefined for AsyncParallelHook
                routesList.getRoutes()
            );
    }

    useNavigationSystemAsync(source, target, callback) {
        const routesList = new List();
        this.hooks.calculateRoutes.callAsync(source, target, routesList, (err) => {
            if (err) return callback(err);
            callback(null, routesList.getRoutes());
        });
    }
}

钩子将编译一个方法,以最有效的方式运行你的插件。它生成的代码取决于:

  • 已注册插件的数量(无,一个,多个)
  • 注册的插件类型(sync, async, promise)
  • 使用的调用方法(sync, async, promise)
  • 参数的数量
  • 是否使用拦截

这确保了最快的执行速度。看到 代码生成 有关运行时编译的详细信息。

插件API

在钩子上注册一个回调函数 tap* 方法。钩子类型决定哪些是有效的(参见 钩类):

  • hook.tap(nameOrOptions, fn) -注册一个同步回调。
  • hook.tapAsync(nameOrOptions, fn) -注册一个基于回调的异步回调。传递给的最后一个参数 fn 是节点风格的回调吗 (err, result)。
  • hook.tapPromise(nameOrOptions, fn) -注册一个返回承诺的异步回调。如果 fn 返回一些不可用的东西,钩子抛出。

第一个参数可以是一个字符串(插件名)或一个选项对象,也允许影响点击运行的顺序:

hook.tap(
    {
        name: "MyPlugin",
        stage: -10, // lower stages run earlier, default is 0
        before: "OtherPlugin" // run before a named tap (string or string[])
    },
    (...args) => {
        /* ... */
    }
);
选项类型描述
namestring必需的。标识用于调试、拦截器和 before 选择。
stagenumber默认为 0. 低级水龙头先于高级水龙头运行。具有相同阶段的抽头按注册顺序运行。
beforestring | string[]该抽头插入在指定抽头之前。未知的名字将被忽略。结合 stage, before 获胜是因为它的目标用户;其他水龙头仍按 stage。

这个 name 也被一些生态系统(如webpack)用于分析和错误消息。在单个监听注册中,以后的拦截器 register 钩子仍然可以替换tap对象(参见 拦截).

hook.withOptions(options)

withOptions 返回钩子周围的facade tap* 方法自动合并 options 进入每一个注册。对于想要预配置 stage 或 before 对于他们添加的所有水龙头:

const lateHook = myCar.hooks.accelerate.withOptions({ stage: 10 });
lateHook.tap("LogAfterOthers", (speed) => console.log("final speed", speed));
// equivalent to: myCar.hooks.accelerate.tap({ name: "LogAfterOthers", stage: 10 }, ...)

返回的对象不公开 call* 方法,因此分发给插件是安全的。

一个可运行的示例显示如何 withOptions 影响水龙头排序:

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

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

hook.tap("Default", (v) => console.log("default", v));

// Pre-configure stage: 10 so all taps registered through `late` run last.
const late = hook.withOptions({ stage: 10 });
late.tap("RunLast", (v) => console.log("last", v));

// Pre-configure stage: -10 so these taps run first. Each facade can also
// be further narrowed via `withOptions`.
const early = hook.withOptions({ stage: -10 });
early.tap("RunFirst", (v) => console.log("first", v));

hook.call(1);
// first 1
// default 1
// last 1

的值 withOptions. 例如, late.tap({ name: "Override", stage: 0 }, fn) 忽略表面的 stage: 10 和寄存器 fn 在阶段 0。

钩类型

每个钩子都可以有一个或几个功能。它们的执行方式取决于钩子类型:

  • 基本钩(没有“瀑布”,“保释”或“循环”的名称)。这个钩子只是调用它在一行中点击的每个函数。

  • 瀑布. 瀑布钩子也会调用一行中的每个被选中的函数。与基本钩子不同,它将每个函数的返回值传递给下一个函数。

  • 保释. 保释挂钩允许提前退出。当任何选中的函数返回任何结果时,保释钩子将停止执行剩余的函数。

  • 循环. 当一个在循环钩子中的插件返回一个未定义的值时,钩子将从第一个插件重新启动。它将循环直到所有插件返回undefined。

此外,钩子可以是同步的,也可以是异步的。为了反映这一点,有“Sync”,“AsyncSeries”和“AsyncParallel”钩子类:

  • 同步. 同步钩子只能通过同步函数调用(使用 myHook.tap()).

  • AsyncSeries. 异步系列钩子可以使用同步的、基于回调的和基于承诺的函数(使用 myHook.tap(), myHook.tapAsync() 和 myHook.tapPromise()). 它们在一行中调用每个异步方法。

  • AsyncParallel. 异步并行钩子也可以使用同步的、基于回调的和基于承诺的函数(使用 myHook.tap(), myHook.tapAsync() 和 myHook.tapPromise()). 但是,它们并行地运行每个异步方法。

钩子类型反映在它的类名中。例如, AsyncSeriesWaterfallHook 允许异步函数并串联运行它们,将每个函数的返回值传递给下一个函数。

钩类

下表总结了9个内置钩子类。对于每个类:

  • 利用方法 是 tapX 可用于注册处理程序的变体。
  • 调用方法 是钩子的所有者触发它的方式。
  • 结果 返回值吗 call (或传递给 callAsync 的回调/解析 promise 调用)。
  • 从抽头返回值 描述从抽头函数返回的值是否有效果。
类利用方法调用方法结果从抽头返回值
SyncHooktapcallundefined忽略了
SyncBailHooktapcall第一个非undefined 价值,或者 undefined使钩子短路
SyncWaterfallHooktapcall最终值(最后一次点击后的第一个参数)作为第一个参数传递给下一个点击
SyncLoopHooktapcallundefined非undefined 从第一次点击开始重新启动循环
AsyncParallelHooktap, tapAsync, tapPromisecallAsync, promiseundefined忽略了
AsyncParallelBailHooktap, tapAsync, tapPromisecallAsync, promise第一个非undefined 价值,或者 undefined使钩子短路
AsyncSeriesHooktap, tapAsync, tapPromisecallAsync, promiseundefined忽略了
AsyncSeriesBailHooktap, tapAsync, tapPromisecallAsync, promise第一个非undefined 价值,或者 undefined使钩子短路
AsyncSeriesLoopHooktap, tapAsync, tapPromisecallAsync, promiseundefined非undefined 从第一次点击开始重新启动循环
AsyncSeriesWaterfallHooktap, tapAsync, tapPromisecallAsync, promise最终值(最后一次点击后的第一个参数)作为第一个参数传递给下一个点击

每个类的详细行为:

SyncHook

一个基本的同步钩子。每个被选中的函数都是按注册顺序调用的,并带有传递给它的参数 call. 调用函数的返回值将被忽略 call 返回 undefined。

  • 利用方法: tap
  • 调用方法: call
  • tapAsync 和 tapPromise 抛出错误。
const hook = new SyncHook(["name"]);
hook.tap("A", (name) => console.log(`hello ${name}`));
hook.tap("B", (name) => console.log(`hi ${name}`));
hook.call("world");
// hello world
// hi world

SyncBailHook

允许提前退出的同步钩子。每个选中的函数都按顺序调用,直到返回一个非undefined 价值;这个值变成的结果 call 剩下的点击会被跳过。如果所有轻击都返回 undefined, call 返回 undefined。

  • 利用方法: tap
  • 调用方法: call
const hook = new SyncBailHook(["value"]);
hook.tap("Negative", (v) => (v < 0 ? "negative" : undefined));
hook.tap("Zero", (v) => (v === 0 ? "zero" : undefined));
hook.tap("Positive", (v) => "positive");

hook.call(-1); // "negative" (later taps skipped)
hook.call(5); // "positive"

SyncWaterfallHook

一种同步钩子,通过其抽头函数来执行一个值。传递给的第一个参数 call 被转发到第一次点击。如果轻击返回非-undefined 值,它将为下一次点击替换该参数;否则,保留之前的值。 call 返回最后一个抽头运行后的值。额外的参数(如果有的话)将不加更改地传递。

  • 利用方法: tap
  • 调用方法: call
const hook = new SyncWaterfallHook(["value"]);
hook.tap("Double", (v) => v * 2);
hook.tap("PlusOne", (v) => v + 1);

hook.call(3); // 7  -> (3 * 2) + 1

SyncLoopHook

一种同步钩子,它不停地重新运行它的水龙头,直到所有水龙头都返回为止 undefined 要一张完整的通行证。每当轻击返回非-时undefined 钩子从第一次点击开始重新启动。 call 返回 undefined。

  • 利用方法: tap
  • 调用方法: call
const hook = new SyncLoopHook(["state"]);
let retries = 3;
hook.tap("Retry", () => {
    if (retries-- > 0) return true; // non-undefined restarts the loop
});
hook.tap("Log", () => console.log("pass"));

hook.call({});
// pass (runs once all taps return undefined)

AsyncParallelHook

一个异步钩子,并行运行它所有的调用函数。当每个tap都发出完成信号(同步返回、回调或承诺解决)时,它完成。返回值和解析值被忽略; callAsync的回调被调用时没有结果,并且 promise() 解析为 undefined. 如果有任何抽头错误,错误将被转发,剩余的抽头仍然完成,但其结果将被丢弃。

  • 利用方法: tap, tapAsync, tapPromise
  • 调用方法: callAsync, promise
const hook = new AsyncParallelHook(["source"]);
hook.tapPromise("Fetch", (src) => fetch(src));
hook.tapAsync("Log", (src, cb) => {
    console.log("fetching", src);
    cb();
});

await hook.promise("https://example.com");

AsyncParallelBailHook

就像 AsyncParallelHook,但其目的是为了摆脱困境。所有抽头函数并行启动;第一个水龙头产生一个非undefined 值(同步的,通过它的回调,或者通过解析它的promise)决定钩子的结果。剩下的水龙头继续运行,但它们的结果被忽略。顺序由轻击注册顺序决定:较早轻击的值优先于较晚轻击的值,即使后一个轻击先完成。

  • 利用方法: tap, tapAsync, tapPromise
  • 调用方法: callAsync, promise
const hook = new AsyncParallelBailHook(["key"]);
hook.tapPromise("Cache", async (key) => cache.get(key));
hook.tapPromise("Db", async (key) => db.lookup(key));

const value = await hook.promise("user:42");
// First non-undefined result (by registration order) wins.

AsyncSeriesHook

一种异步钩子,它一个接一个地运行选定的函数,等待每个函数完成后再开始下一个函数。结果被忽略; callAsync的回调被调用时没有结果,并且 promise() 解析为 undefined. 第一个错误将终止该系列。

  • 利用方法: tap, tapAsync, tapPromise
  • 调用方法: callAsync, promise
const hook = new AsyncSeriesHook(["request"]);
hook.tapPromise("Authenticate", async (req) => authenticate(req));
hook.tapPromise("Log", async (req) => logger.info(req.url));

await hook.promise(request);

AsyncSeriesBailHook

允许提前退出的异步系列钩子。选中的函数一个接一个地运行;一旦产生非-undefined 值,则该值成为钩子的结果,并跳过剩余的点击。

  • 利用方法: tap, tapAsync, tapPromise
  • 调用方法: callAsync, promise
const hook = new AsyncSeriesBailHook(["id"]);
hook.tapPromise("Memory", async (id) => memory.get(id));
hook.tapPromise("Disk", async (id) => disk.read(id));

const value = await hook.promise("doc-1");
// Stops at the first tap that produces a value.

AsyncSeriesLoopHook

循环的异步系列钩子。选中的函数一个接一个地运行;每当轻击产生非-时undefined 钩子从第一次点击开始重新启动。当一个完整的传递产生时,钩子完成 undefined 从每一个水龙头。结果总是 undefined。

  • 利用方法: tap, tapAsync, tapPromise
  • 调用方法: callAsync, promise
const hook = new AsyncSeriesLoopHook(["job"]);
hook.tapPromise("Process", async (job) => {
    const more = await job.step();
    if (more) return true; // restart the loop
});

await hook.promise(job);

AsyncSeriesWaterfallHook

一个异步系列钩子,通过它的抽头执行一个值。传递给的第一个参数 callAsync / promise 被转发到第一次点击。水龙头不是undefined 返回/回调/解析值为下一次点击替换它; undefined 保持先前的值。钩子在最后一次点击后完成该值。

  • 利用方法: tap, tapAsync, tapPromise
  • 调用方法: callAsync, promise
const hook = new AsyncSeriesWaterfallHook(["source"]);
hook.tapPromise("Read", async (src) => fs.readFile(src, "utf8"));
hook.tapPromise("Trim", async (text) => text.trim());

const output = await hook.promise("./input.txt");

拦截

所有钩子都公开一个 intercept(interceptor) 方法。拦截器是一个普通对象,它的方法在钩子生命周期中的特定点被调用。拦截器在监听之前按注册顺序调用,对于记录、跟踪、分析或重新映射监听选项很有用。

myCar.hooks.calculateRoutes.intercept({
    name: "LoggingInterceptor",
    call: (source, target, routesList) => {
        console.log("Starting to calculate routes");
    },
    tap: (tapInfo) => {
        // tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ..., stage: 0 }
        console.log(`${tapInfo.name} is running`);
    },
    register: (tapInfo) => {
        // Called once per tap (and for each tap already registered when the
        // interceptor is added). Return a new tapInfo object to replace it.
        console.log(`${tapInfo.name} is registered`);

        return tapInfo;
    }
});
处理程序签名当它运行时
call(...args) => void在钩子开始执行它的水龙头之前。接收传递给的参数 call / callAsync / promise。
tap(tap: Tap) => void在每个水龙头运行之前。这个 tap 对象是快照-忽略突变。
loop(...args) => void在a的每次迭代开始时 SyncLoopHook / AsyncSeriesLoopHook。
error(err: Error) => void每当tap抛出、拒绝或调用回调时出现错误。
result(result: any) => void当保释或瀑布钩子产生一个值时,或者当tap为循环钩子产生一个值时。
done() => void当钩子成功完成时(没有错误,没有提前保释)。
register(tap: Tap) => Tap | undefined在注册时每次监听一次(包括在添加拦截器之前存在的监听)。返回new Tap 对象来替换它。
namestring生态系统调试时使用的可选标签。
contextboolean选择加入共享的 context 对象。看到 上下文。

添加拦截器会使钩子的编译调用函数失效 call / callAsync / promise 重新编译它,以便将新的拦截器编入其中。

上下文

插件和拦截器可以选择访问一个可选的 context 对象,该对象可用于将任意值传递给后续的插件和拦截器。

myCar.hooks.accelerate.intercept({
    context: true,
    tap: (context, tapInfo) => {
        // tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
        console.log(`${tapInfo.name} is doing it's job`);

        // `context` starts as an empty object if at least one plugin uses `context: true`.
        // If no plugins use `context: true`, then `context` is undefined.
        if (context) {
            // Arbitrary properties can be added to `context`, which plugins can then access.
            context.hasMuffler = true;
        }
    }
});

myCar.hooks.accelerate.tap(
    {
        name: "NoisePlugin",
        context: true
    },
    (context, newSpeed) => {
        if (context && context.hasMuffler) {
            console.log("Silence...");
        } else {
            console.log("Vroom!");
        }
    }
);

HookMap

一个 HookMap 是一个帮助器类,它惰性地为每个键创建钩子。构造函数接受一个工厂函数;第一次通过请求密钥 for(key),则调用工厂,并缓存生成的钩子。

const keyedHook = new HookMap((key) => new SyncHook(["arg"]));

插件使用 for(key) 获取特定键的钩子(按需创建),然后 tap 像往常一样:

keyedHook.for("some-key").tap("MyPlugin", (arg) => {
    /* ... */
});
keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => {
    /* ... */
});
keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => {
    /* ... */
});

的所有者 HookMap 使用 get(key) 查找现有的钩子而不创建钩子。这通常是在调用端首选的,这样没有插件关心的键永远不会被实现:

const hook = keyedHook.get("some-key");
if (hook !== undefined) {
    hook.callAsync("arg", (err) => {
        /* ... */
    });
}

一个 HookMap 也可以被拦截。 intercept({ factory }) 包装工厂,以便您可以自定义或替换为每个新键返回的钩子。

代码生成

Tapable不会在调用时迭代轻击。相反,是第一次 call, callAsync 或 promise 在钩子被修改后调用,钩子会使用 new Function(...) 并在实例上缓存它。这就是README中“代码中的eval”的含义:钩子的调度逻辑作为字符串生成,并转化为引擎可以内联和优化的真正的JavaScript函数。

生成的函数被定制为:

  • 调用类型 -主人是否打来电话 call (同步) callAsync (回调),或 promise. 每一种都会产生不同的骨架。 promise() 把尸体裹起来 new Promise((_resolve, _reject) => { ... })。
  • 利用类型 -对于每次点击,生成器都会发出正确的调用模式:直接调用 tap的节点式回调包装 tapAsync, .then(...) 链接为 tapPromise。
  • 钩类 - - - - - - SyncHook 发出直线序列的呼叫; SyncBailHook 发出提前返回支票; SyncWaterfallHook 通过调用线程化一个值;循环钩子将主体包裹在一个重新进入的循环中; AsyncParallel* 扇出水龙头并计算完井次数; AsyncSeries* 链。
  • 拦截器 -如果附加了拦截器,调用它们的 call/tap/loop/error/result/done 处理程序被拼接到生成的主体中;否则,它们不需要任何成本。
  • 上下文 ——一个 _context 对象只有在至少有一个tap或拦截器选择使用 context: true。
  • 参数数量 -生成的代码硬编码了当钩子被构造时声明的参数的数量,所以没有 arguments/rest处理发生在运行时。

每当钩子的形状发生变化时,编译的函数就失效(重置回一次性“重新编译然后调用”trampoline) tap* 或 intercept 呼叫。因此,稳态调用直接通过缓存的函数运行,没有每个tap分支。

为什么这很重要?

  • 您只需为您使用的功能付费。一个没有拦截器、只同步的钩子编译成一个简短的直接函数调用序列。
  • 调试钩子意味着读取生成的源代码。如果你想看的话, Hook.prototype.compile 返回 new Function(...) 结果-日志 hook._createCall("sync").toString() (或 "async" / "promise")检查尸体。
  • 因为分派是由代码生成的,所以钩子的行为是在编译时完全确定的。在注册后改变点击选项(例如,更改 stage 在现有的 Tap 对象)在引起重新编译之前不会对抽头重新排序。

钩/ HookMap接口

Public(任何持有钩子引用的人都可以调用,比如插件):

interface Hook {
    tap: (name: string | Tap, fn: (context?, ...args) => Result) => void;
    tapAsync: (
        name: string | Tap,
        fn: (
            context?,
            ...args,
            callback: (err: Error | null, result: Result) => void
        ) => void
    ) => void;
    tapPromise: (
        name: string | Tap,
        fn: (context?, ...args) => Promise<Result>
    ) => void;
    intercept: (interceptor: HookInterceptor) => void;
    withOptions: (
        options: TapOptions
    ) => Omit<Hook, "call" | "callAsync" | "promise">;
}

interface HookInterceptor {
    name?: string;
    call?: (context?, ...args) => void;
    loop?: (context?, ...args) => void;
    tap?: (context?, tap: Tap) => void;
    error?: (err: Error) => void;
    result?: (result: any) => void;
    done?: () => void;
    register?: (tap: Tap) => Tap | undefined;
    context?: boolean;
}

interface HookMap {
    for: (key: any) => Hook;
    intercept: (interceptor: HookMapInterceptor) => void;
}

interface HookMapInterceptor {
    factory: (key: any, hook: Hook) => Hook;
}

interface Tap {
    name: string;
    type: "sync" | "async" | "promise";
    fn: Function;
    stage: number;
    context: boolean;
    before?: string | Array<string>;
}

受保护(只针对包含钩子的类——它拥有触发钩子的权利):

interface Hook {
    isUsed: () => boolean;
    call: (...args) => Result;
    promise: (...args) => Promise<Result>;
    callAsync: (
        ...args,
        callback: (err: Error | null, result: Result) => void
    ) => void;
}

interface HookMap {
    get: (key: any) => Hook | undefined;
    for: (key: any) => Hook;
}

isUsed() 返回 true 当钩子至少注册了一个tap或interceptor时。当没有插件在监听时,钩子所有者可以使用它来跳过昂贵的参数准备:

class Car {
    // ...
    setSpeed(newSpeed) {
        if (this.hooks.accelerate.isUsed()) {
            this.hooks.accelerate.call(newSpeed);
        }
    }
    // ...
}

MultiHook

一个 MultiHook 是一个向前的钩子式立面 tap, tapAsync, tapPromise, intercept, withOptions 同时连接几个底层钩子。它不会暴露 call* 方法——只有包装钩子的所有者决定它们何时运行。这是类公开“发生在这些事件中的任何一个”监听表面的典型方式,而无需插件将自己单独连接到每个钩子。

把一个龙头向几个钩子扇形展开

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

class Car {
    constructor() {
        const accelerate = new SyncHook(["newSpeed"]);
        const brake = new SyncHook();
        this.hooks = {
            accelerate,
            brake,
            // `anyMovement` is not a real hook — it simply re-registers taps
            // on both `accelerate` and `brake`.
            anyMovement: new MultiHook([accelerate, brake])
        };
    }
}

const car = new Car();
car.hooks.anyMovement.tap("Telemetry", () => console.log("car moved"));

car.hooks.accelerate.call(42); // "car moved"
car.hooks.brake.call(); // "car moved"

这个 MultiHook 没有自己的状态:上面的水龙头在里面结束 accelerate.taps 和 brake.taps。

转发异步轻接

tapAsync / tapPromise 转发到每个包装的钩子——确保它们都是兼容的是插件的工作。注册一个 tapPromise 在一个 MultiHook 这就结束了。 SyncHook 将在注册时间抛出钩子。

const build = new AsyncSeriesHook(["stats"]);
const rebuild = new AsyncSeriesHook(["stats"]);
const anyBuild = new MultiHook([build, rebuild]);

anyBuild.tapPromise("Report", async (stats) => report.send(stats));

共享拦截器和选项

intercept 和 withOptions 也都转发了,所以呢 MultiHook 可以用来将相同的拦截器或预配置的选项附加到一组钩子上:

const anyBuild = new MultiHook([build, rebuild]);

anyBuild.intercept({
    call: () => console.log("build started"),
    done: () => console.log("build done")
});

// Every tap added through `late` is staged late on both underlying hooks.
const late = anyBuild.withOptions({ stage: 10 });
late.tap("RunLast", () => {
    /* ... */
});

isUsed

isUsed() 返回 true 如果任何一个包裹的钩子至少有一个tap或interceptor,当没有人在监听它们时,所有者可以便宜地跳过工作:

if (this.hooks.anyMovement.isUsed()) {
    // expensive telemetry payload is only built when a plugin actually cares
    this.hooks.accelerate.call(computeSpeed());
}

版本与发布记录

tapable 共发布 54 个版本,首次发布于 2013/01/21;过去 12 个月发布了 3 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.3.3latest2026/04/216 个月前
2.3.22026/03/247 个月前
2.3.12026/03/237 个月前
2.3.02025/10/021.1 年前
2.2.32025/08/231.2 年前
2.2.22025/05/201.5 年前

其他发布通道:beta → 2.0.0-beta.11。

关于 tapable 的常见问题

tapable 是什么?

只是一个插件的小模块。

如何安装 tapable?

在项目目录运行 npm install tapable,或使用 pnpm add / yarn add / bun add。

tapable 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

tapable 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

tapable 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=6。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

tapable 可以商用吗?

tapable 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

tapable 的下载量有多少?

根据 npm 官方下载统计,tapable 在 2026/10/07 – 2026/11/05 这 30 天内被下载 626,296,851 次,日均约 20,876,562 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

tapable 还在维护吗?

最新版本 2.3.3 发布于 2026/04/21(6 个月前);至今共发布 54 个版本,其中过去 12 个月发布了 3 个;首次发布于 2013/01/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。