首页 > 应用开发 > IT综合 > Webpack 基石 tapable 揭秘
Webpack 基于 tapable 构建了其复杂庞大的流程管理系统,基于 tapable 的架构不仅解耦了流程节点和流程的具体实现,还保证了 Webpack 强大的扩展能力;学习掌握tapable,有助于我们深入理解 Webpack。
The tapable package expose many Hook classes,which can be used to create hooks for plugins.
tapable 提供了一些用于创建插件的钩子类。
个人觉得 tapable 是一个基于事件的流程管理工具。
tapable于2020.9.18发布了v2.0版本。此文章内容也是基于v2.0版本。
tapable有两个基类:Hook和HookCodeFactory。Hook类定义了Hook interface(Hook接口), HookCodeFactoruy类的作用是动态生成一个流程控制函数。生成函数的方式是通过我们熟悉的New Function(arg,functionBody)。

tapable会动态生成一个可执行函数来控制钩子函数的执行。我们以SyncHook的使用来举一个例子,比如我们有这样的一段代码:
|
1
2
3
4
5 |
// SyncHook使用import
{ SyncHook } from
'../lib'
;const syncHook =
new
SyncHook();syncHook.tap(
'x'
, () => console.log(
'x done'
));syncHook.tap(
'y'
, () => console.log(
'y done'
)); |
上面的代码只是注册好了钩子函数,要让函数被执行,还需要触发事件(执行调用)
|
1 |
syncHook.call(); |
syncHook.call()在调用时会生成这样的一个动态函数:
|
1
2
3
4
5
6
7
8
9 |
function
anonymous() {
"use strict"
;
var
_context;
var
_x =
this
._x;
var
_fn0 = _x[0];
_fn0();
var
_fn1 = _x[1];
_fn1();} |
这个函数的代码非常简单:就是从一个数组中取出函数,依次执行。注意:不同的调用方式,最终生成的的动态函数是不同的。如果把调用代码改成:
|
1 |
syncHook.callAsync( () => {console.log(
'all done'
)} ) |
那么最终生成的动态函数是这样的:
|
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 |
function
anonymous(_callback) {
"use strict"
;
var
_context;
var
_x =
this
._x;
var
_fn0 = _x[0];
var
_hasError0 =
false
;
try
{
_fn0();
}
catch
(_err) {
_hasError0 =
true
;
_callback(_err);
}
if
(!_hasError0) {
var
_fn1 = _x[1];
var
_hasError1 =
false
;
try
{
_fn1();
}
catch
(_err) {
_hasError1 =
true
;
_callback(_err);
}
if
(!_hasError1) {
_callback();
}
}} |
这个动态函数相对于前面的动态函数要复杂一些,但仔细一看,执行逻辑也非常简单:同样是从数组中取出函数,依次执行;只不过这次多了2个逻辑:
通过研究最终生成的动态函数,我们不难发现:动态函数的模板特性非常突出。前面的例子中,我们只注册了x,y2个钩子,这个模板保证了当我们注册任意个钩子时,动态函数也能方便地生成出来,具有非常强的扩展能力。
那么这些动态函数是如何生成的呢?其实Hook的生成流程是一样的。hook.tap只是完成参数准备,真正的动态函数生成是在调用后(水龙头打开后)。完整流程如下:

在tapablev2中,一共提供了12种类型的Hook,接下来,通过梳理Hook怎么执行和Hook完成回调何时执行2方面来理解tapable提供的这些Hook类。
钩子函数按次序依次全部执行;如果有Hook回调,则Hook回调在最后执行。
|
1
2
3
4
5
6
7
8
9
10
11 |
const syncHook =
new
SyncHook();syncHook.tap(
'x'
, () => console.log(
'x done'
));syncHook.tap(
'y'
, () => console.log(
'y done'
));syncHook.callAsync(() => { console.log(
'all done'
) }); /*输出:x doney doneall done*/ |
钩子函数按次序执行。如果某一步钩子返回了非undefined,则后面的钩子不再执行;如果有Hook回调,直接执行Hook回调。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14 |
const hook =
new
SyncBailHook(); hook.tap(
'x'
, () => {
console.log(
'x done'
);
return
false
;
// 返回了非undefined,y不会执行});hook.tap(
'y'
, () => console.log(
'y done'
));hook.callAsync(() => { console.log(
'all done'
) }); /*输出:x doneall done*/ |
钩子函数按次序全部执行。后一个钩子的参数是前一个钩子的返回值。最后执行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 |
const hook =
new
SyncWaterfallHook([
'count'
]); hook.tap(
'x'
, (count) => {
let
result = count + 1;
console.log(
'x done'
, result);
return
result;});hook.tap(
'y'
, (count) => {
let
result = count * 2;
console.log(
'y done'
, result);
return
result;});hook.tap(
'z'
, (count) => {
console.log(
'z done & show result'
, count);});hook.callAsync(5, () => { console.log(
'all done'
) }); /*输出:x done 6y done 12z done & show result 12all done*/ |
钩子函数按次序全部执行。每一步的钩子都会循环执行,直到返回值为undefined,再开始执行下一个钩子。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 |
const hook =
new
SyncLoopHook(); let
flag = 0;let
flag1 = 5; hook.tap(
'x'
, () => {
flag = flag + 1;
if
(flag >= 5) {
// 执行5次,再执行 y
console.log(
'x done'
);
return
undefined;
}
else
{
console.log(
'x loop'
);
return
true
;
}});hook.tap(
'y'
, () => {
flag1 = flag1 * 2;
if
(flag1 >= 20) {
// 执行2次,再执行 z
console.log(
'y done'
);
return
undefined;
}
else
{
console.log(
'y loop'
);
return
true
;
}});hook.tap(
'z'
, () => {
console.log(
'z done'
);
// z直接返回了undefined,所以只执行1次
return
undefined;}); hook.callAsync(() => { console.log(
'all done'
) }); /*输出:x loopx loopx loopx loopx doney loopx doney donez doneall done
*/ |
钩子函数异步并行全部执行。所有钩子的回调返回后,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 |
const hook =
new
AsyncParallelHook([
'arg1'
]);const start = Date.now(); hook.tapAsync(
'x'
, (arg1, callback) => {
console.log(
'x done'
, arg1);
setTimeout(() => {
callback();
}, 1000)});hook.tapAsync(
'y'
, (arg1, callback) => {
console.log(
'y done'
, arg1);
setTimeout(() => {
callback();
}, 2000)});hook.tapAsync(
'z'
, (arg1, callback) => {
console.log(
'z done'
, arg1);
setTimeout(() => {
callback();
}, 3000)}); hook.callAsync(1, () => {
console.log(`all done。 耗时:${Date.now() - start}`);}); /*输出:x done 1y done 1z done 1all done。 耗时:3006*/ |
钩子函数异步串行全部执行,会保证钩子执行顺序,上一个钩子结束后,下一个才会开始。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 |
const hook =
new
AsyncSeriesHook([
'arg1'
]);const start = Date.now(); hook.tapAsync(
'x'
, (arg1, callback) => {
console.log(
'x done'
, ++arg1);
setTimeout(() => {
callback();
}, 1000)});hook.tapAsync(
'y'
, (arg1, callback) => {
console.log(
'y done'
, arg1);
setTimeout(() => {
callback();
}, 2000)}); hook.tapAsync(
'z'
, (arg1, callback) => {
console.log(
'z done'
, arg1);
setTimeout(() => {
callback();
}, 3000)}); hook.callAsync(1, () => {
console.log(`all done。 耗时:${Date.now() - start}`);}); /*输出:x done 2y done 1z done 1all done。 耗时:6008*/ |
钩子异步并行执行,即钩子都会执行,但只要有一个钩子返回了非undefined,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 |
const hook =
new
AsyncParallelBailHook([
'arg1'
]);const start = Date.now(); hook.tapAsync(
'x'
, (arg1, callback) => {
console.log(
'x done'
, arg1);
setTimeout(() => {
callback();
}, 1000)});hook.tapAsync(
'y'
, (arg1, callback) => {
console.log(
'y done'
, arg1);
setTimeout(() => {
callback(
true
);
}, 2000)}); hook.tapAsync(
'z'
, (arg1, callback) => {
console.log(
'z done'
, arg1);
setTimeout(() => {
callback();
}, 3000)}); hook.callAsync(1, () => {
console.log(`all done。 耗时:${Date.now() - start}`);});/*输出:x done 1y done 1z done 1all done。 耗时:2006
*/ |
钩子函数异步串行执行。但只要有一个钩子返回了非undefined,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 |
const hook =
new
AsyncSeriesBailHook([
'arg1'
]);const start = Date.now(); hook.tapAsync(
'x'
, (arg1, callback) => {
console.log(
'x done'
, ++arg1);
setTimeout(() => {
callback(
true
);
// y 不会执行
}, 1000);});hook.tapAsync(
'y'
, (arg1, callback) => {
console.log(
'y done'
, arg1);
setTimeout(() => {
callback();
}, 2000);}); hook.callAsync(1, () => {
console.log(`all done。 耗时:${Date.now() - start}`);}); /*输出:x done 2all done。 耗时:1006
*/ |
钩子函数异步串行全部执行,上一个钩子返回的参数会传给下一个钩子。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 |
const hook =
new
AsyncSeriesWaterfallHook([
'arg'
]);const start = Date.now(); hook.tapAsync(
'x'
, (arg, callback) => {
console.log(
'x done'
, arg);
setTimeout(() => {
callback(
null
, arg + 1);
}, 1000)},); hook.tapAsync(
'y'
, (arg, callback) => {
console.log(
'y done'
, arg);
setTimeout(() => {
callback(
null
,
true
);
// 不会阻止 z 的执行
}, 2000)}); hook.tapAsync(
'z'
, (arg, callback) => {
console.log(
'z done'
, arg);
callback();}); hook.callAsync(1, (x, arg) => {
console.log(`all done, arg: ${arg}。 耗时:${Date.now() - start}`);}); /*输出:x done 1y done 2z done trueall done, arg: true。 耗时:3010
*/ |
钩子函数异步串行全部执行,某一步钩子函数会循环执行到返回非undefined,才会开始下一个钩子。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 |
const hook =
new
AsyncSeriesLoopHook([
'arg'
]);const start = Date.now();let
counter = 0; hook.tapAsync(
'x'
, (arg, callback) => {
console.log(
'x done'
, arg);
counter++;
setTimeout(() => {
if
(counter >= 5) {
callback(
null
, undefined);
// 开始执行 y
}
else
{
callback(
null
, ++arg);
// callback(err, result)
}
}, 1000)},); hook.tapAsync(
'y'
, (arg, callback) => {
console.log(
'y done'
, arg);
setTimeout(() => {
callback(
null
, undefined);
}, 2000)}); hook.tapAsync(
'z'
, (arg, callback) => {
console.log(
'z done'
, arg);
callback(
null
, undefined);}); hook.callAsync(
'AsyncSeriesLoopHook'
, (x, arg) => {
console.log(`all done, arg: ${arg}。 耗时:${Date.now() - start}`);}); /*x done AsyncSeriesLoopHookx done AsyncSeriesLoopHookx done AsyncSeriesLoopHookx done AsyncSeriesLoopHookx done AsyncSeriesLoopHooky done AsyncSeriesLoopHookz done AsyncSeriesLoopHookall done, arg: undefined。 耗时:7014*/ |
主要作用是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 |
const hookMap =
new
HookMap(() =>
new
SyncHook([
'x'
])); hookMap.
for
(
'key1'
).tap(
'p1'
,
function
() {
console.log(
'key1-1:'
, ...arguments);});hookMap.
for
(
'key1'
).tap(
'p2'
,
function
() {
console.log(
'key1-2:'
, ...arguments);});hookMap.
for
(
'key2'
).tap(
'p3'
,
function
() {
console.log(
'key2'
, ...arguments);}); const hook = hookMap.get(
'key1'
); if
( hook !== undefined ) {
hook.call(
'hello'
,
function
() {
console.log(
''
, ...arguments)
});} /*输出:key1-1: hellokey1-2: hello*/ |
MultiHook主要用于向Hook批量注册钩子函数。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 |
const syncHook =
new
SyncHook([
'x'
]);const syncLoopHook =
new
SyncLoopHook([
'y'
]);const mutiHook =
new
MultiHook([syncHook, syncLoopHook]); // 向多个hook注册同一个函数mutiHook.tap(
'plugin'
, (arg) => {
console.log(
'common plugin'
, arg);}); // 执行函数for
(const hook of mutiHook.hooks) {
hook.callAsync(
'hello'
, () => {
console.log(
'hook all done'
);
});} |
以上Hook又可以抽象为以下几类:
注意钩子函数返回值判断是和undefined对比,而不是和假值对比(null, false)
Hook也可以按同步、异步划分:
Hook实例默认都有都有tap, tapAsync, tapPromise三个注册钩子回调的方法,不同注册方法生成的动态函数是不一样的。当然也并不是所有Hook都支持这几个方法,比如SyncHook不支持tapAsync, tapPromise。
Hook默认有call, callAsync,promise来执行回调。但并不是所有Hook都会有这几个方法,比如SyncHook不支持callAsync和promise。
我们先复习下jQuery.ajax()的常规用法(大概用法是这样,咱不纠结每个参数都正确):
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 |
jQuery.ajax({
url:
'api/request/url'
,
beforeSend:
function
(config) {
return
config;
// 返回false会取消此次请求发送
},
success:
function
(data) {
// 成功逻辑
}
error:
function
(err) {
// 失败逻辑
},
complete:
function
() {
// 成功,失败都会执行的逻辑
}}); |
jQuery.ajax整个流程做了这么几件事:

同时,我们借鉴axios的做法,将beforeSend改为transformRequest,加入transformResponse,再加上统一的请求loading和默认的错误处理,这时我们整个ajax流程如下:

上面的代码,我们可以继续优化:把每个流程点都抽象成一个独立插件,最后再串联起来。如处理loading展示的独立成LoadingPlugin.js,返回预处理transformResponse独立成TransformResponsePlugin.js,这样我们可能得到这么一个结构:

这个结构就和大名鼎鼎的Webpack组织插件的形式基本一致了。接下来我们看看tapable在Webpack中的应用,看一看为什么tapable能够称为Webpack基石。
如果你需要强大的流程管理能力,可以考虑基于tapable去做架构设计。
作者:vivo-Ou Fujun
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69912579/viewspace-2761486/ ,如需转载,请注明出处,否则将追究法律责任。