Tapable
以下代码来自V2.2.1
Tapable是webpack最重要的库了,如果你需要阅读源码,需要写一个webpack plugin,那么最好先了解Tapable是什么
Tapable其实就是一个EventEmitter库,但它相比普通的EventEmitter,它还支持异步的Event,这样就可以支持异步插件。
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = Tapable
上面是来自Tapable的库导出的函数方法,除了Sync的方法,还有Async。
SyncHook
const hook = new SyncHook(["arg1", "arg2"]);
hook.tap('LoggerPlugin', (arg1, arg2) => { console.log(arg1 , arg2) })
hook.tap('SumPlugin', (arg1, arg2) => { console.log(arg1 + arg2) })
hook.call(1,2)
// 1, 2
// 3
同步的钩子很简单,调用call会执行执行所有被tapped
的钩子。 同步方法除了普通的钩子还有其他的,比如:
- SyncWaterfallHook WaterfallHook可以将上一个回调返回值传入下一个钩子
- SyncBailHook 如果上一个回调有返回值,BailHook可以立即退出不再执行后面的钩子
- SyncLoopHook SyncLoopHook在回调有返回值的时候会从第一个钩子重新开始执行,一直循环到所有钩子都返回undefined为止
AsyncHook
异步的钩子和同步钩子差异的地方在于有两个不同的关键字眼,Paralle
和Series
。一个可以并行执行钩子,一个可以按顺序执行钩子。
const asyncHook = new AsyncParallelHook(["arg"])
asyncHook.tapAsync('SetTimeoutPlugin', (arg, cb) => {
setTimeout(() => {
console.log('macrotask runed')
cb()
}, 2000)
})
asyncHook.tapPromise('PromisePlugin', (arg, cb) => {
//return promise
return new Promise((resolve) => {
setTimeout(() => {
console.log('microtask runed');
resolve();
}, 1000);
})
})
asyncHook.callAsync("arg", () => {
console.log("task clearout")
})
// microtask runed
// macrotask runed
// task clearout
可以看到有两种注入异步钩子的方式,一种需要调用回调参数cb,一种需要返回Promise的形式。
上面的AsyncParallelHook
以一种并行的方式执行,所以执行顺序跟着回调走.另外一种AsyncSeriesHook
根据注册顺序走,只有前面的钩子执行回调函数才会继续下一个钩子,也就是说// macrotask runed
会被先打印
Interception和context
hooks还提供了Interception,可以拦截hooks的行为。Context能做为上下文在Interception中传递
asyncHook.intercept({
call: (...arg) => {
console.log("Starting asyncHook");
},
loop: (...args) => {
console.log('restart looping')
},
tap: (tapInfo) => {
console.log('new hooks be tapped')
}
register: (tapInfo) => {
// tapInfo = { type: "promise", name: "PromisePlugin", fn: ... }
console.log(`${tapInfo.name} is doing its job`);
return tapInfo; // may return a new tapInfo object
}
})
//Context
hooks.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;
}
}
});
hooks.tap({
name: "NoisePlugin",
context: true
}, (context, newSpeed) => {
if (context && context.hasMuffler) {
console.log("Silence...");
} else {
console.log("Vroom!");
}
});
HookMap和MultiHook
Tapable还提供了其他辅助方法
//HookMap
const { HookMap } = require("tapable");
const keyedHook = new HookMap(key => new SyncHook(["arg"]))
keyedHook.for("some-key").tap("MyPlugin", (arg) => { /* ... */ });
keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ });
keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => { /* ... */ });
const hook = keyedHook.get("some-key");
if(hook !== undefined) {
hook.callAsync("arg", err => { /* ... */ });
}
//MultiHook
const { MultiHook } = require("tapable");
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
源码解析
Hook
hooks文件实现了需对对外暴露的方法,比如tap,tapAsync,tapAsync,还有一些处理intercept的方法。
_tap(type, options, fn) {
if (typeof options === "string") {
options = {
name: options.trim()
};
} else if (typeof options !== "object" || options === null) {
throw new Error("Invalid tap options");
}
if (typeof options.name !== "string" || options.name === "") {
throw new Error("Missing name for tap");
}
if (typeof options.context !== "undefined") {
deprecateContext();
}
options = Object.assign({ type, fn }, options);
options = this._runRegisterInterceptors(options);
this._insert(options);
}
所有的tap都会调用_tap函数。首先处理options,然后调用_runRegisterInterceptors
和_insert
const CALL_DELEGATE = function(...args) {
this.call = this._createCall("sync");
return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function(...args) {
this.callAsync = this._createCall("async");
return this.callAsync(...args);
};
const PROMISE_DELEGATE = function(...args) {
this.promise = this._createCall("promise");
return this.promise(...args);
};
class Hook {
constructor(args = [], name = undefined) {
this._call = CALL_DELEGATE;
this.call = CALL_DELEGATE;
this._callAsync = CALL_ASYNC_DELEGATE;
this.callAsync = CALL_ASYNC_DELEGATE;
this._promise = PROMISE_DELEGATE;
this.promise = PROMISE_DELEGATE;
}
}
hooks里的调用都会有一个副本,先说明这是因为暴露给用户的函数是通过new Function生成使用的,所以需要保存生产函数的方法,下次再重新赋值
_runRegisterInterceptors(options) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
const newOptions = interceptor.register(options);
if (newOptions !== undefined) {
options = newOptions;
}
}
}
return options;
}
_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
_insert(item) {
this._resetCompilation();
let before;
if (typeof item.before === "string") {
before = new Set([item.before]);
} else if (Array.isArray(item.before)) {
before = new Set(item.before);
}
let stage = 0;
if (typeof item.stage === "number") {
stage = item.stage;
}
let i = this.taps.length;
while (i > 0) {
i--;
const x = this.taps[i];
this.taps[i + 1] = x;
const xStage = x.stage || 0;
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
if (before.size > 0) {
continue;
}
}
if (xStage > stage) {
continue;
}
i++;
break;
}
this.taps[i] = item;
}
_runRegisterInterceptors
实现了interceptor.register
的功能,用于返回新的options。insert第一行会运行_resetCompilation去重新赋值,前面提前说明了call
,callAsync
,promise
的方法是生成出来的,所以每次tap的时候都需要重新去生成函数。
SyncHook和AsyncParallelHook
github.com/webpack/tap… 每个hooks的代码都大同小异,它们都extends HookCodeFactory
,并且返回实例化的Hooks。
HookCodeFactory
所有的hooks都扩展于HookCodeFactory
,当hooks.call执行时,会调用HookCodeFactory.prototype.create方法通过new Function生成调用函数。
class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
this._args = undefined;
}
create(options) {
this.init(options);
let fn;
switch (this.options.type) {
case "sync":
fn = new Function(
this.args(),
'"use strict";\n' +
this.header() +
this.contentWithInterceptors({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
);
break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict";\n' +
this.header() +
this.contentWithInterceptors({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => "_callback();\n"
})
);
break;
case "promise":
let errorHelperUsed = false;
const content = this.contentWithInterceptors({
onError: err => {
errorHelperUsed = true;
return `_error(${err});\n`;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
let code = "";
code += '"use strict";\n';
code += this.header();
code += "return new Promise((function(_resolve, _reject) {\n";
if (errorHelperUsed) {
code += "var _sync = true;\n";
code += "function _error(_err) {\n";
code += "if(_sync)\n";
code +=
"_resolve(Promise.resolve().then((function() { throw _err; })));\n";
code += "else\n";
code += "_reject(_err);\n";
code += "};\n";
}
code += content;
if (errorHelperUsed) {
code += "_sync = false;\n";
}
code += "}));\n";
fn = new Function(this.args(), code);
break;
}
this.deinit();
return fn;
}
}
call会调用compile生成函数并保存起来,只有intercept和tap才会重新去编译。
const hook = new SyncHook(["arg1", "arg2"]);
hook.tap('LoggerPlugin', (arg1, arg2) => { console.log(arg1 , arg2) })
hook.tap('SumPlugin', (arg1, arg2) => { console.log(arg1 + arg2) })
hook.intercept({call: ()=> {}})
hook.call(1,2)
// compile后的匿名函数
ƒ anonymous(arg1, arg2) {
"use strict";
var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
_interceptors[0].call(arg1, arg2); //调用拦截器
var _fn0 = _x[0];
_fn0(arg1, arg2); //LoggerPlugin callback
var _fn1 = _x[1]; //SumPlugin callback
_fn1(arg1, arg2);
}
通过字符拼接和new Function,可以创造出异步或者同步代码,并且还能够插入拦截代码。
结尾
Tapable是webpack比较重要的库,基本大部分重要的模块都有它,并需要依赖它来与其他功能模块通信,来完成webpack构建。理解Tapable能够让你更容易去阅读webpack源码或者你只是想写一个Plugin,比如应该在compiler
的哪个钩子阶段才能拿到编译产生的compiltion
,应该以怎样的方式注入一个钩子进去修改构建产物的信息等等。这种解耦的方式能够很好帮助webpack扩展功能,但是因为源码实现问题,导致async hook不能被await,会产生callback hell,让阅读变得困难