起因
搜索引擎搜索 tapable 中文文档, 你会看见各种翻译, 点进去一看, 确实是官方的文档翻译过来的, 但是 webpack 的文档确实还有很多需要改进的地方, 既然是开源的为什么不去 github 上的 tapable 库看呢, 一看, 确实, 比 webpack 文档上的描述得清楚得多.
tapable 是一个类似于 nodejs 的 EventEmitter 的库, 主要是控制钩子函数的发布与订阅, 控制着 webpack 的插件系.webpack 的本质就是一系列的插件运行.
Tapable
Tapable 库 提供了很多的钩子类, 这些类可以为插件创建钩子
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require(“tapable”);
安装
npm install –save tapable
使用
所有的钩子构造函数, 都接受一个可选的参数,(这个参数最好是数组, 不是 tapable 内部也把他变成数组), 这是一个参数的字符串名字列表
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 consument
// 使用 tap 方法添加一个消费者,(生产者消费者模式)
myCar.hooks.brake.tap(“WarningLampPlugin”, () => warningLamp.on());
这需要你传一个名字去标记这个插件:
你可以接收参数
myCar.hooks.accelerate.tap(“LoggerPlugin”, newSpeed => console.log(`Accelerating to ${newSpeed}`));
在同步钩子中, tap 是唯一的绑定方法, 异步钩子通常支持异步插件
// promise: 绑定 promise 钩子的 API
myCar.hooks.calculateRoutes.tapPromise(“GoogleMapsPlugin”, (source, target, routesList) => {
// return a promise
return google.maps.findRoute(source, target).then(route => {
routesList.add(route);
});
});
// tapAsync: 绑定异步钩子的 API
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
// tap: 绑定同步钩子的 API
myCar.hooks.calculateRoutes.tap(“CachedRoutesPlugin”, (source, target, routesList) => {
const cachedRoute = cache.get(source, target);
if(cachedRoute)
routesList.add(cachedRoute);
})
类需要调用被声明的那些钩子
class Car {
/* … */
setSpeed(newSpeed) {
// call(xx) 传参调用同步钩子的 API
this.hooks.accelerate.call(newSpeed);
}
useNavigationSystemPromise(source, target) {
const routesList = new List();
// 调用 promise 钩子 (钩子返回一个 promise) 的 API
return this.hooks.calculateRoutes.promise(source, target, routesList).then(() => {
return routesList.getRoutes();
});
}
useNavigationSystemAsync(source, target, callback) {
const routesList = new List();
// 调用异步钩子 API
this.hooks.calculateRoutes.callAsync(source, target, routesList, err => {
if(err) return callback(err);
callback(null, routesList.getRoutes());
});
}
}
钩子会用最有效率的方式去编译 (构建) 一个运行你的插件的方法, 他生成的代码依赖于一下几点:
你注册的插件的个数.
你注册插件的类型.
你使用的调用方法(call, promise, async) // 其实这个类型已经包括了
钩子参数的个数 // 就是你 new xxxHook([‘ooo’]) 传入的参数
是否应用了拦截器(拦截器下面有讲)
这些确定了尽可能快的执行.
钩子类型
每一个钩子都可以 tap 一个或者多个函数, 他们如果运行, 取决于他们的钩子类型
基本的钩子, (钩子类名没有 waterfall, Bail, 或者 Loop 的), 这个钩子只会简单的调用每个 tap 进去的函数
Waterfall, 一个 waterfall 钩子, 也会调用每个 tap 进去的函数, 不同的是, 他会从每一个函数传一个返回的值到下一个函数
Bail, Bail 钩子允许更早的退出, 当任何一个 tap 进去的函数, 返回任何值, bail 类会停止执行其他的函数执行.(类似 Promise.reace())
Loop, TODO(我 …. 这里也没描述, 应该是写文档得时候 还没想好这个要怎么写, 我尝试看他代码去补全, 不过可能需要点时间.)
此外, 钩子可以是同步的, 也可以是异步的,Sync, AsyncSeries 和 AsyncParallel 钩子就反应了这个问题
Sync, 一个同步钩子只能 tap 同步函数, 不然会报错.
AsyncSeries, 一个 async-series 钩子 可以 tap 同步钩子, 基于回调的钩子 (我估计是类似 chunk 的东西) 和一个基于 promise 的钩子(使用 myHook.tap(), myHook.tapAsync() 和 myHook.tapPromise().). 他会按顺序的调用每个方法.
AsyncParallel, 一个 async-parallel 钩子跟上面的 async-series 一样 不同的是他会把异步钩子并行执行(并行执行就是把异步钩子全部一起开启, 不按顺序执行).
拦截器(interception)
所有钩子都提供额外的拦截器 API
// 注册一个拦截器
myCar.hooks.calculateRoutes.intercept({
call: (source, target, routesList) => {
console.log(“Starting to calculate routes”);
},
register: (tapInfo) => {
// tapInfo = {type: “promise”, name: “GoogleMapsPlugin”, fn: …}
console.log(`${tapInfo.name} is doing its job`);
return tapInfo; // may return a new tapInfo object
}
})
call:(…args) => void 当你的钩子触发之前,(就是 call()之前), 就会触发这个函数, 你可以访问钩子的参数. 多个钩子执行一次
tap: (tap: Tap) => void 每个钩子执行之前(多个钩子执行多个), 就会触发这个函数
loop:(…args) => void 这个会为你的每一个循环钩子 (LoopHook, 就是类型到 Loop 的) 触发, 具体什么时候没说
register:(tap: Tap) => Tap | undefined 每添加一个 Tap 都会触发 你 interceptor 上的 register, 你下一个拦截器的 register 函数得到的参数 取决于你上一个 register 返回的值, 所以你最好返回一个 tap 钩子.
Context(上下文)
插件和拦截器都可以选择加入一个可选的 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`.
// 如果最少有一个插件使用 `context` 那么 context 一开始是一个空的对象
// If no plugins use `context: true`, then `context` is undefined
// 如过 tap 进去的插件没有使用 `context` 的 那么内部的 `context` 一开始就是 undefined
if (context) {
// Arbitrary properties can be added to `context`, which plugins can then access.
// 任意属性都可以添加到 `context`, 插件可以访问到这些属性
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 是一个 Hooks 映射的帮助类
const keyedHook = new HookMap(key => new SyncHook([“arg”]))
keyedHook.tap(“some-key”, “MyPlugin”, (arg) => {/* … */});
keyedHook.tapAsync(“some-key”, “MyPlugin”, (arg, callback) => {/* … */});
keyedHook.tapPromise(“some-key”, “MyPlugin”, (arg) => {/* … */});
const hook = keyedHook.get(“some-key”);
if(hook !== undefined) {
hook.callAsync(“arg”, err => { /* … */});
}
钩子映射接口(HookMap interface)
Public(权限公开的):
interface Hook {
tap: (name: string | Tap, fn: (context?, …args) => Result) => void,
tapAsync: (name: string | Tap, fn: (context?, …args, callback: (err, result: Result) => void) => void) => void,
tapPromise: (name: string | Tap, fn: (context?, …args) => Promise<Result>) => void,
intercept: (interceptor: HookInterceptor) => void
}
interface HookInterceptor {
call: (context?, …args) => void,
loop: (context?, …args) => void,
tap: (context?, tap: Tap) => void,
register: (tap: Tap) => Tap,
context: boolean
}
interface HookMap {
for: (key: any) => Hook,
tap: (key: any, name: string | Tap, fn: (context?, …args) => Result) => void,
tapAsync: (key: any, name: string | Tap, fn: (context?, …args, callback: (err, result: Result) => void) => void) => void,
tapPromise: (key: any, name: string | Tap, fn: (context?, …args) => Promise<Result>) => void,
intercept: (interceptor: HookMapInterceptor) => void
}
interface HookMapInterceptor {
factory: (key: any, hook: Hook) => Hook
}
interface Tap {
name: string,
type: string
fn: Function,
stage: number,
context: boolean
}
Protected(保护的权限), 只用于类包含的 (里面的) 钩子
interface Hook {
isUsed: () => boolean,
call: (…args) => Result,
promise: (…args) => Promise<Result>,
callAsync: (…args, callback: (err, result: Result) => void) => void,
}
interface HookMap {
get: (key: any) => Hook | undefined,
for: (key: any) => Hook
}
MultiHook
把其他的 Hook 重定向 (转化) 成为一个 MultiHook
const {MultiHook} = require(“tapable”);
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
OK 所有的内容我都已翻译完成.
其中有很多不是直译, 这样写下来感觉就是按照原文的脉络重新写了一遍 …., 应该能更清楚明白, 要不是怕丢脸我就给个原创了, 哈哈.
之后, 我还会写一篇完整的原创解析, 直击源码, 搞定 tapable, 完全了解 webpack 插件系统(webpack 本来就是一个插件的事件流), 好久没写原创了. 我自己也很期待.