Webpack Tapable Source Code

Introduction

Webpack 作为主流的打包工具,提供了自定义扩展的loaderplugin,丰富了周边生态。如果想自己写一个plugin,需要对相关的hooks有所了解,而 webpack 对hooks的实现则是建立在tapable这个库上的,本篇文章希望通过对tapable源码的梳理,以加深webpack的相关知识以及部分设计模式的理解。

How to use tapable

tapable和常见发布订阅模式实现的代码用法基本一致。基本的SyncHook使用如下所示:

1
2
3
4
5
6
7
8
9
10
const { SyncHook } = require('tapable');

const hook = new SyncHook(['name']); //实例化一个hook

hook.tap('plugin1', name => {
//注册回调
console.log(name);
});

hook.call('xbrave'); //触发回调,打印'xbrave'

Hook

Hook类作为其它 Hook 类的基类,整体上把发布订阅的内容抽象了出来,整体代码不多,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
'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初始化为:

1
2
3
compile(options) {
throw new Error("Abstract: should be overridden");
}

如果子类不自行实现compile则会报错

subscribe

内部的_tap()方法主要作用是整合传入的options,然后调用内部的this._insert(options)方法,将相关回调注册到内部的this.tapsthis.insert()内部还针对options内的stagebefore做了一次排序,关于stagebefore的作用有兴趣的可以了解一下ref

HookCodeFactory

HookCodeFactory的主要作用是针对不同类型的 Hook,相应的动态生成所需要的compile()函数。

setup()

注意到Hook类初始化时的_x并没有指定内容,这里对内部的_x重新赋值:

1
2
3
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}

new Function()

一般来说,js 中创建函数一般都是函数声明和函数表达式居多,但是还有其他不太常用的方法, 比如new Function(),示例如下:

1
2
3
4
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类型,精简后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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的源码比较简单,只有几十行,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
'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;

整体顺序为:

  1. SyncHookCodeFactory类继承HookCodeFactory基类,并自行实现content()方法用于动态生成代码
  2. SyncHook类继承Hook基类,并自行实现tapAsync(),tapPromise()compile()方法用于后续调用
  3. SyncHook.prototype = null;prototype指向null TODO: why?

关于订阅tap这块的内容,在之前讲Hook基类的时候描述过,这里不再赘述,这里主要关注点在如何动态生成call()方法。

content()

content()主要是生成最终函数的主要内容,相关方法及其后续调用顺序比较深,整体的调用顺序为:
HookCodeFactory.create() -> HookCodeFactory.contentWithInterceptors() -> SyncHookCodeFactory.content() -> HookCodeFactory.callTapsSeries() -> HookCodeFactory.callTap()
我们重点关注callTap()"sync"部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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并执行,生成的字符串类似于

1
2
3
4
var _fn0 = _x[0];
_fn0();
var _fn1 = _x[1];
_fn1();

至于为什么不用 for 循环,应该是考虑到简单可扩展

回到之前的setup()部分,为什么要执行instance._x = options.taps.map(t => t.fn);,和header()放在一起看就比较明晰了,
header()相关的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()及其相关的方法生成的结果类似如下形式:

1
2
3
4
5
6
7
'use strict';
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0();
var _fn1 = _x[1];
_fn1();

再通过new Function()最终动态生成了SyncHookcall方法

Summary

  1. tapable主要基于Hook类的发布订阅模式,以及HookCodeFactory类的工厂模式来抽象代码,这两个类抽象比较高,搞懂这两个类后续就比较好理解了

  2. tapable基于new Function()来动态创建call,callAsync,promise方法

Reference

MDN Function() constructor

Query regarding coding paradigm used in this library

Comments