Introduction
Webpack作为主流的打包工具,提供了自定义扩展的loader
和plugin
,丰富了周边生态。如果想自己写一个plugin
,需要对相关的hooks
有所了解,而webpack对hooks
的实现则是建立在tapable
这个库上的,本篇文章希望通过对tapable
源码的梳理,以加深webpack
的相关知识以及部分设计模式的理解。
How to use tapable
tapable
和常见发布订阅模式实现的代码用法基本一致。基本的SyncHook
使用如下所示:
const { SyncHook } = require('tapable');
const hook = new SyncHook(['name']); //实例化一个hook
hook.tap('plugin1', (name) => {//注册回调
console.log(name);
})
hook.call('xbrave'); //触发回调,打印'xbrave'
Hook
Hook类作为其它Hook类的基类,整体上把发布订阅的内容抽象了出来,整体代码不多,如下所示:
"use strict";
const util = require("util");
const deprecateContext = util.deprecate(() => {},
"Hook.context is deprecated and will be removed");
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._args = args;
this.name = name;
this.taps = [];
this.interceptors = [];
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;
this._x = undefined;
this.compile = this.compile;
this.tap = this.tap;
this.tapAsync = this.tapAsync;
this.tapPromise = this.tapPromise;
}
compile(options) {
throw new Error("Abstract: should be overridden");
}
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
_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(options, fn) {
this._tap("sync", options, fn);
}
tapAsync(options, fn) {
this._tap("async", options, fn);
}
tapPromise(options, fn) {
this._tap("promise", options, fn);
}
_runRegisterInterceptors(options) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
const newOptions = interceptor.register(options);
if (newOptions !== undefined) {
options = newOptions;
}
}
}
return options;
}
withOptions(options) {
const mergeOptions = opt =>
Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);
return {
name: this.name,
tap: (opt, fn) => this.tap(mergeOptions(opt), fn),
tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn),
tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn),
intercept: interceptor => this.intercept(interceptor),
isUsed: () => this.isUsed(),
withOptions: opt => this.withOptions(mergeOptions(opt))
};
}
isUsed() {
return this.taps.length > 0 || this.interceptors.length > 0;
}
intercept(interceptor) {
this._resetCompilation();
this.interceptors.push(Object.assign({}, interceptor));
if (interceptor.register) {
for (let i = 0; i < this.taps.length; i++) {
this.taps[i] = interceptor.register(this.taps[i]);
}
}
}
_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;
}
}
Object.setPrototypeOf(Hook.prototype, null);
module.exports = Hook;
init
这里需要注意的是this.call
,this.callAsync
等属性初始化的时候指向的是一个函数,主要是考虑到其它类继承时的初始化问题,其它类可以自行实现this.compile
方法,同时compile
初始化为:
compile(options) {
throw new Error("Abstract: should be overridden");
}
如果子类不自行实现compile
则会报错
subscribe
内部的_tap()
方法主要作用是整合传入的options
,然后调用内部的this._insert(options)
方法,将相关回调注册到内部的this.taps
,this.insert()
内部还针对options
内的stage
和before
做了一次排序,关于stage
和before
的作用有兴趣的可以了解一下ref
HookCodeFactory
HookCodeFactory的主要作用是针对不同类型的Hook,相应的动态生成所需要的compile()
函数。
setup()
注意到Hook
类初始化时的_x
并没有指定内容,这里对内部的_x
重新赋值:
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
new Function()
一般来说,js中创建函数一般都是函数声明和函数表达式居多,但是还有其他不太常用的方法,比如new Function()
,示例如下:
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));
// expected output: 8
上述的参数'a'
和'b'
可以换为'a,b'
即以英文逗号分割的字符串作为多个参数传入。以new Function()
形式声明函数的好处是可以使用字符串动态创建函数,但是可读性比较差,日常开发中的代码不建议这么做。
create()
回到tapable
源码中, create()
方法就是用来动态生成函数的,一共有三种类型,sync
,async
,promise
本篇文章主要关注sync
类型,精简后的代码如下:
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;
}
this.deinit();
return fn;
}
可以看到create()
方法最主要的还是调用new Function()
来动态创建fn
并返回
SyncHook
基于以上的基础,我们来看一下SyncHook是如何实现的。
SyncHook
的源码比较简单,只有几十行,如下所示:
"use strict";
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
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;
module.exports = SyncHook;
整体顺序为:
SyncHookCodeFactory
类继承HookCodeFactory
基类,并自行实现content()
方法用于动态生成代码SyncHook
类继承Hook
基类,并自行实现tapAsync()
,tapPromise()
与compile()
方法用于后续调用SyncHook.prototype = null;
将prototype
指向null
TODO: why?
关于订阅tap
这块的内容,在之前讲Hook
基类的时候描述过,这里不再赘述,这里主要关注点在如何动态生成call()
方法。
content()
content()主要是生成最终函数的主要内容,相关方法及其后续调用顺序比较深,整体的调用顺序为:
HookCodeFactory.create()
-> HookCodeFactory.contentWithInterceptors()
-> SyncHookCodeFactory.content()
-> HookCodeFactory.callTapsSeries()
-> HookCodeFactory.callTap()
我们重点关注callTap()
的"sync"
部分:
case "sync":
if (!rethrowIfPossible) {
code += `var _hasError${tapIndex} = false;\n`;
code += "try {\n";
}
if (onResult) {
code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})});\n`;
} else {
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})});\n`;
}
if (!rethrowIfPossible) {
code += "} catch(_err) {\n";
code += `_hasError${tapIndex} = true;\n`;
code += onError("_err");
code += "}\n";
code += `if(!_hasError${tapIndex}) {\n`;
}
if (onResult) {
code += onResult(`_result${tapIndex}`);
}
if (onDone) {
code += onDone();
}
if (!rethrowIfPossible) {
code += "}\n";
}
break;
这里是将每个之前tap
注册的函数从this._x
拿出来,依次赋值给_fn
并执行,生成的字符串类似于
var _fn0 = _x[0];
_fn0();
var _fn1 = _x[1];
_fn1();
至于为什么不用for循环,应该是考虑到简单可扩展
header()
回到之前的setup()
部分,为什么要执行instance._x = options.taps.map(t => t.fn);
,和header()
放在一起看就比较明晰了,
header()
相关的代码如下:
header() {
let code = "";
if (this.needContext()) {
code += "var _context = {};\n";
} else {
code += "var _context;\n";
}
code += "var _x = this._x;\n";
if (this.options.interceptors.length > 0) {
code += "var _taps = this.taps;\n";
code += "var _interceptors = this.interceptors;\n";
}
return code;
}
可以看出来主要就是生成var _x = this._x;
, 而这个_x
就是callTap()
中用到的_x
,
最终header()
和content()
及其相关的方法生成的结果类似如下形式:
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0();
var _fn1 = _x[1];
_fn1();
再通过new Function()
最终动态生成了SyncHook
的call
方法
Summary
-
tapable
主要基于Hook
类的发布订阅模式,以及HookCodeFactory
类的工厂模式来抽象代码,这两个类抽象比较高,搞懂这两个类后续就比较好理解了 -
tapable
基于new Function()
来动态创建call
,callAsync
,promise
方法