tapable
tapable 导出了 9 个 hooks
- SyncHook
- SyncBailHook
- SyncWaterfallHook
- SyncLoopHook
- AsyncParallelHook
- AsyncParallelBailHook
- AsyncSeriesHook
- AsyncSeriesBailHook
- AsyncSeriesWaterfallHook
上述 9 个 hooks 都继承自 Hook 这个 class
tapable Hook 解析
hook 对外提供了 isUsed
call
promise
callAsync
compile
tap
tapAsync
tapPromise
intercept
这些方法
其中 tap
开头的方法是用来订阅事件的,call
promise
callAsync
是用来触发事件的,isUsed
返回了一个 boolean
值用来标记当前 hook
中注册的事件是否被执行完成。
isUsed
源码
isUsed() { return this.taps.length > 0 || this.interceptors.length > 0; }
tap
tapAsync
tapPromise
这三个方法第一个参数传入可以支持传入 string
(一般是指 plugin 的名称) 或者一个 Tap
类型,第二个参数是一个回调用来接收事件被 emit
时的调用。
export interface Tap {
name: string; // 事件名称,一般就是 plugin 的名字
type: TapType; // 支持三种类型 'sync' 'async' 'promise'
fn: Function;
stage: number;
context: boolean;
}
call
promise
callAsync
这三个方法在传入参数的时候是依赖于 hook
被实例化的时候传入的 args
数组占位符的数量的,如下示例:
const sync = new SyncHook(['arg1', 'arg2']) // 'arg1' 'arg2' 为参数占位符
sync.tap('Test', (arg1, arg2) => {
console.log(arg1, arg2) // a2
})
sync.call('a', '2')
其中 promise
调用会返回一个 Promise
,callAsync
默认支持传入一个 callback
。
Sync
开头的 hook
不支持使用 tapAsync
和 tapPromise
,可以看下述的以 SyncHook
的源码为例
const TAP_ASYNC = () => {
throw new Error("tapAsync is not supported on a SyncHook");
};
const TAP_PROMISE = () => {
throw new Error("tapPromise is not supported on a SyncHook");
};
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
SyncHook.prototype = null;
在这里面我们可以看到 tapAsync
和 tapPromise
是被重写了直接 throw error
了
一个简单的使用示范
下面的例子会给大家带来一个简单地示范
class TapableTest {
constructor() {
this.hooks = {
sync: new SyncHook(['context', 'hi']),
syncBail: new SyncBailHook(),
syncLoop: new SyncLoopHook(),
syncWaterfall: new SyncWaterfallHook(['syncwaterfall']),
asyncParallel: new AsyncParallelHook(),
asyncParallelBail: new AsyncParallelBailHook(),
asyncSeries: new AsyncSeriesHook(),
asyncSeriesBail: new AsyncSeriesBailHook(),
asyncSeriesWaterfall: new AsyncSeriesWaterfallHook(['asyncwaterfall'])
}
}
emitSync() {
this.hooks.sync.call(this, err => {
console.log(this.hooks.sync.promise)
console.log(err)
})
}
emitAyncSeries() {
this.hooks.asyncSeries.callAsync(err => {
if (err) console.log(err)
})
}
}
const test = new TapableTest()
test.hooks.sync.tap('TestPlugin', (context, callback) => {
console.log('trigger: ', context)
callback(new Error('this is sync error'))
})
test.hooks.asyncSeries.tapAsync('AsyncSeriesPlugin', callback => {
callback(new Error('this is async series error'))
})
test.emitSync()
test.emitAyncSeries()
上述的运行结果可以这查看 runkit
下面来聊一聊 webpack 中的插件是如何依赖 tapable 的
webpack 插件被注入的时机
当我们定义了 webpack
的配置文件后,webpack
会根据这些配置生成一个或多个 compiler
,而插件就是在创建 compiler
时被添加到 webpack
的整个运行期间的, 可以看下述源码:(相关源码可以在 webpack
lib 下的 webpack.js
中找到)
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
我们可以看到遍历 options.plugins
这一段,这一段分了两种情况来进行插件的插入
- 我们的 plugin 可以以函数的方式被
webpack
调用,也就是说我们可以用函数来写插件,这个函数的作用域是当前的compiler
,函数也会接收到一个compiler
- 可以传入一个包含
apply
方法的对象实例,apply
方法会被传入compiler
所以这也就解释了为什么我们的插件需要 new
出来之后传入到 webpack
进入 Compiler 一探究竟
上一个中我们了解到了 plugins
是何时被注入的,我们可以看到在 plugin
的注入时传入了当前被实例化出来的 Compiler
,所以现在我们需要了解下 Compiler
中做了什么
进入 Compiler.js
(也在 lib 中)我们可以第一时间看到 Compiler
的 constructor
中定义了一个庞大的 hooks
:
this.hooks = Object.freeze({
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<[Stats]>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {SyncHook<[Stats]>} */
afterDone: new SyncHook(["stats"]),
/** @type {AsyncSeriesHook<[]>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[Compiler]>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compiler]>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
assetEmitted: new AsyncSeriesHook(["file", "info"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterEmit: new AsyncSeriesHook(["compilation"])
...
})
看到这些 hook
是不是很熟悉,全是 tapable
中的 hook
,webpack 正是依赖于这些复杂的构建 hook
而完成了我们的代码构建,所以在我们编写 plugin
时就可以利用这些 hook
来完成我们的特殊需求。
比如我们经常用到的 HtmlWebpackPlugin
,我们可以看下他是如何运行的,在 HtmlWebpackPlugin
的 apply
中我们可以找到这样一段代码:
compiler.hooks.emit.tapAsync('HtmlWebpackPlugin', (compiler, callback) => {
...
})
说明 HtmlWebpackPlugin
是利用了 Compiler
的 emit
的 hook
来完成的
通过深入了解,webpack
是在庞大的插件上运行的,他自己内置了很多插件
上述内容如有错误,请指正