Webpack 基石 tapable 揭秘

Webpack 基于 tapable 构建了其复杂庞大的流程管理系统,基于 tapable 的架构不仅解耦了流程节点和流程的具体实现,还保证了 Webpack 强大的扩展能力;学习掌握tapable,有助于我们深入理解 Webpack。

一、tapable是什么?

The tapable package expose many Hook classes,which can be used to create hooks for plugins.

tapable 提供了一些用于创建插件的钩子类。

个人觉得 tapable 是一个基于事件的流程管理工具。

二、tapable架构原理和执行过程

tapable于2020.9.18发布了v2.0版本。此文章内容也是基于v2.0版本。

2.1 代码架构

tapable有两个基类:Hook和HookCodeFactory。Hook类定义了Hook interface(Hook接口), HookCodeFactoruy类的作用是动态生成一个流程控制函数。生成函数的方式是通过我们熟悉的New Function(arg,functionBody)。

2.2 执行流程

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只是完成参数准备,真正的动态函数生成是在调用后(水龙头打开后)。完整流程如下:

三、Hook 类型详解

在tapablev2中,一共提供了12种类型的Hook,接下来,通过梳理Hook怎么执行和Hook完成回调何时执行2方面来理解tapable提供的这些Hook类。

3.1 SyncHook

钩子函数按次序依次全部执行;如果有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 done
y done
all done
*/

3.2 SyncBailHook

钩子函数按次序执行。如果某一步钩子返回了非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 done
all done
*/

3.3 SyncWaterfallHook

钩子函数按次序全部执行。后一个钩子的参数是前一个钩子的返回值。最后执行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 6
y done 12
z done & show result 12
all done
*/

3.4 SyncLoopHook

钩子函数按次序全部执行。每一步的钩子都会循环执行,直到返回值为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 loop
x loop
x loop
x loop
x done
y loop
x done
y done
z done
all done
 */

3.5  AsyncParallelHook

钩子函数异步并行全部执行。所有钩子的回调返回后,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 1
y done 1
z done 1
all done。 耗时:3006
*/

3.6 AsyncSeriesHook

钩子函数异步串行全部执行,会保证钩子执行顺序,上一个钩子结束后,下一个才会开始。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 2
y done 1
z done 1
all done。 耗时:6008
*/

3.7 AsyncParallelBailHook

钩子异步并行执行,即钩子都会执行,但只要有一个钩子返回了非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 1
y done 1
z done 1
all done。 耗时:2006
 */

3.8 AsyncSeriesBailHook

钩子函数异步串行执行。但只要有一个钩子返回了非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 2
all done。 耗时:1006
 */

3.9 AsyncSeriesWaterfallHook

钩子函数异步串行全部执行,上一个钩子返回的参数会传给下一个钩子。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 1
y done 2
z done true
all done, arg: true。 耗时:3010
 */

3.10 AsyncSeriesLoopHook

钩子函数异步串行全部执行,某一步钩子函数会循环执行到返回非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 AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
x done AsyncSeriesLoopHook
y done AsyncSeriesLoopHook
z done AsyncSeriesLoopHook
all done, arg: undefined。 耗时:7014
*/

3.11 HookMap

主要作用是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: hello
key1-2: hello
*/

3.12 MultiHook

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又可以抽象为以下几类:

  • xxxBailHook:根据前一步钩子函数的返回值是否是undefined来决定要不要执行下一步钩子:如果某一步返回了非undefined,则后面的钩子不在执行。
  • xxxWaterfallHook:上一步钩子函数返回值就是下一步函数的参数。
  • xxxLoopHook:钩子函数循环执行,直到返回值为undefined。

注意钩子函数返回值判断是和undefined对比,而不是和假值对比(null, false)

Hook也可以按同步、异步划分:

  • syncXXX:同步钩子
  • asyncXXX:异步钩子

Hook实例默认都有都有tap, tapAsync, tapPromise三个注册钩子回调的方法,不同注册方法生成的动态函数是不一样的。当然也并不是所有Hook都支持这几个方法,比如SyncHook不支持tapAsync, tapPromise。

Hook默认有call, callAsync,promise来执行回调。但并不是所有Hook都会有这几个方法,比如SyncHook不支持callAsync和promise。

四、实践应用

4.1 基于 tapable 实现类 jQuery.ajax()封装

我们先复习下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整个流程做了这么几件事:

  • 在请求真正发送前,beforeSend提供了请求配置预处理的钩子。如果预处理函数返回false,能取消此次请求的发送。
  • 请求成功(服务端数据返回后)执行success函数逻辑。
  • 如果请求失败,则执行error函数逻辑。
  • 最终,统一执行complete函数逻辑,无论请求成功还是失败。

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

4.2 简单版的实现

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
const { SyncHook, AsyncSeriesWaterfallHook } = require('tapable');
  
class Service {
    constructor() {
        this.hooks = {
            loading:  new SyncHook(['show']),
            transformRequest: new AsyncSeriesWaterfallHook(['config', 'transformFunction']),
            request: new SyncHook(['config']),
            transformResponse: new AsyncSeriesWaterfallHook(['config', 'response', 'transformFunction']),
            success: new SyncHook(['data']),
            fail: new SyncHook(['config', 'error']),
            finally: new SyncHook(['config', 'xhr'])
        };
  
        this.init();
    }
    init() {
        // 解耦后的任务逻辑
        this.hooks.loading.tap('LoadingToggle', (show) => {
            if (show) {
                console.log('展示ajax-loading');
            } else {
                console.log('关闭ajax-loading');
            }
        });
  
        this.hooks.transformRequest.tapAsync('DoTransformRequest', (
            config,
            transformFunction= (d) => {
                d.__transformRequest = true;
                return d;
            },
            cb
        ) => {
            console.log(`transformRequest拦截器:Origin:${JSON.stringify(config)};`);
            config = transformFunction(config);
            console.log(`transformRequest拦截器:after:${JSON.stringify(config)};`);
            cb(null, config);
        });
  
        this.hooks.transformResponse.tapAsync('DoTransformResponse', (
            config,
            data,
            transformFunction= (d) => {
                d.__transformResponse = true;
                return d;
            },
            cb
        ) => {
            console.log(`transformResponse拦截器:Origin:${JSON.stringify(config)};`);
            data = transformFunction(data);
            console.log(`transformResponse拦截器:After:${JSON.stringify(data)}`);
            cb(null, data);
        });
  
        this.hooks.request.tap('DoRequest', (config) => {
            console.log(`发送请求配置:${JSON.stringify(config)}`);
  
            // 模拟数据返回
            const sucData = {
                code: 0,
                data: {
                    list: ['X50 Pro', 'IQOO Neo'],
                    user: 'jack'
                },
                message: '请求成功'
            };
  
            const errData = {
                code: 100030,
                message: '未登录,请重新登录'
            };
  
            if (Date.now() % 2 === 0) {
                this.hooks.transformResponse.callAsync(config, sucData, undefined, () => {
                    this.hooks.success.callAsync(sucData, () => {
                        this.hooks.finally.call(config, sucData);
                    });
                });
            } else {
                this.hooks.fail.callAsync(config, errData, () => {
                    this.hooks.finally.call(config, errData);
                });
            }
        });
    }
    start(config) {
        this.config = config;
  
        /*
            通过Hook调用定制串联流程
            1. 先 transformRequest
            2. 处理 loading
            3. 发起 request
         */
        this.hooks.transformRequest.callAsync(this.config, undefined, () => {
            this.hooks.loading.callAsync(this.config.loading, () => {
            });
  
            this.hooks.request.call(this.config);
        });
    }
}
  
const s = new Service();
  
s.hooks.success.tap('RenderList', (res) => {
    const { data } = res;
    console.log(`列表数据:${JSON.stringify(data.list)}`);
});
  
s.hooks.success.tap('UpdateUserInfo', (res) => {
    const { data } = res;
    console.log(`用户信息:${JSON.stringify(data.user)}`);
});
  
s.hooks.fail.tap('HandlerError', (config, error) => {
    console.log(`请求失败了,config=${JSON.stringify(config)},error=${JSON.stringify(error)}`);
});
  
s.hooks.finally.tap('DoFinally', (config, data) => {
    console.log(`DoFinally,config=${JSON.stringify(config)},data=${JSON.stringify(data)}`);
});
  
s.start({
    base: '/cgi/cms/',
    loading: true
});
  
/*
成功返回输出:
transformRequest拦截器:Origin:{"base":"/cgi/cms/","loading":true};
transformRequest拦截器:after:{"base":"/cgi/cms/","loading":true,"__transformRequest":true};
展示ajax-loading
发送请求配置:{"base":"/cgi/cms/","loading":true,"__transformRequest":true}
transformResponse拦截器:Origin:{"base":"/cgi/cms/","loading":true,"__transformRequest":true};
transformResponse拦截器:After:{"code":0,"data":{"list":["X50 Pro","IQOO Neo"],"user":"jack"},"message":"请求成功","__transformResponse":true}
列表数据:["X50 Pro","IQOO Neo"]
用户信息:"jack"
DoFinally,config={"base":"/cgi/cms/","loading":true,"__transformRequest":true},data={"code":0,"data":{"list":["X50 Pro","IQOO Neo"],"user":"jack"},"message":"请求成功","__transformResponse":true}
*/

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

这个结构就和大名鼎鼎的Webpack组织插件的形式基本一致了。接下来我们看看tapable在Webpack中的应用,看一看为什么tapable能够称为Webpack基石。

4.3 tapable在 Webpack中的应用

  • Webpack中,一切皆插件(Hook)。
  • Webpack通过tapable将这些插件串起来,组成固定流程。
  • tapable解耦了流程任务和具体实现,同时提供了强大的扩展能力:拿到Hook,就能插入自己的逻辑。(我们平时写Webpack插件,就是找到对应的Hook去,然后注册我们自己的钩子函数。这样就方便地把我们自定义逻辑,插入到了Webpack任务流程中了)。

如果你需要强大的流程管理能力,可以考虑基于tapable去做架构设计。

五、小结

  • tapable是一个流程管理工具。
  • 提供了10种类型Hook,可以很方便地让我们去实现复杂的业务流程。
  • tapable核心原理是基于配置,通过new Function方式,实时动态生成函数表达式去执行,从而完成逻辑
  • tapable通过串联流程节点来实现流程控制,保证了流程的准确有序。
  • 每个流程节点可以任意注册钩子函数,从而提供了强大的扩展能力。
  • tapable是Webpack基石,它支撑了Webpack庞大的插件系统,又保证了这些插件的有序运行。
  • 如果你也正在做一个复杂的流程系统(任务系统),可以考虑用tapable来管理你的流程。

 

作者:vivo-Ou Fujun

posted @   vivo互联网技术  阅读(399)  评论(0编辑  收藏  举报
编辑推荐:
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示