乐趣区

关于javascript: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 的应用来举一个例子,比方咱们有这样的一段代码:

// SyncHook 应用
import {SyncHook} from '../lib';
const syncHook = new SyncHook();
syncHook.tap('x', () => console.log('x done'));
syncHook.tap('y', () => console.log('y done'));

下面的代码只是注册好了钩子函数,要让函数被执行,还须要触发事件(执行调用)

syncHook.call();

syncHook.call() 在调用时会生成这样的一个动静函数:

function anonymous() {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0();
    var _fn1 = _x[1];
    _fn1();}

这个函数的代码非常简单:就是从一个数组中取出函数,顺次执行。留神:不同的调用形式,最终生成的的动静函数是不同的。如果把调用代码改成:

syncHook.callAsync(() => {console.log('all done')} )

那么最终生成的动静函数是这样的:

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 回调在最初执行。

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 回调。

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 回调。

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 回调最初执行。

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 回调才执行。

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 回调最初执行。

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 回调会间接执行。

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 回调就执行,也就是说有的钩子可能不会执行。

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 回调会在所有钩子回调返回后才执行。

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 回调会在所有钩子回调实现后执行。

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 组批量调用。

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 批量注册钩子函数。

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() 的惯例用法(大略用法是这样,咱不纠结每个参数都正确):

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 简略版的实现

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

退出移动版