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

Webpack 基石 tapable 揭秘的更多相关文章

  1. Webpack的tapable 为什么要使用 new Funtion 来生成静态代码

    为了保持代码的单态(monomorphism). 这涉及到了js引擎优化的一些问题, tapable从1.0.0版本开始就用new Function来生成静态代码最后来来执行, 以确保得到最优执行效率 ...

  2. 揭秘webpack plugin

    前言 Plugin(插件) 是 webpack 生态的的一个关键部分.它为社区提供了一种强大的方法来扩展 webpack 和开发 webpack 的编译过程.这篇文章将尝试探索 webpack plu ...

  3. 关于webpack,打包时遇到的错误

    最近在研究webpack这玩意,然后遇到一个问题,执行npm run build的时候,出现下面这个问题,各种搜索后,各种尝试,都没解决 运行时报错ERROR in ./src/app.vue Mod ...

  4. Tapable 0.2.8 入门

    [原文:Tapable 0.2.8 入门] tapable是webpack的核心框架(4.0以上版本的API已经发生了变化),是一个基于事件流的框架,或者叫做发布订阅模式,或观察者模式,webpack ...

  5. 从0开始编写webpack插件

    1. 前言 插件(plugins)是webpack中的一等功臣.正是由于有了诸多插件的存在,才使得webpack无所不能.在webpack源码中也是使用了大量的内部插件,插件要是用的好,可以让你的工作 ...

  6. 从 Tapable 中得到的启发

    Tapable Why Tapable 前端开发中 Webpack 本质上是基于事件流的运行机制,它的工作流程是将特定的任务分发到指定的事件钩子中去完成.而实现这一切的核心就是 tapable,Web ...

  7. webpack打包原理

    什么是 webpack ? 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler).当 webpack 处理应用程序时,它会递归地构建一个依 ...

  8. 初探webpack之编写plugin

    初探webpack之编写plugin webpack通过plugin机制让其使用更加灵活,以适应各种应用场景,当然也大大增加了webpack的复杂性,在webpack运行的生命周期中会广播出许多事件, ...

  9. OBKoro1的2020年年终总结

    前言 一晃眼2020年马上就要过去了,今年感觉过的特别快. 工作已经三年了,之前都没有写过年终总结,结果造成了下面这个现象: 回首过去的几年,记忆已经很模糊了,需要很用力才能想起过去一部分往事. 人生 ...

随机推荐

  1. EFCore学习记录--数据访问技术人门

    1.安装Microsoft.EntityFrameworkCore.Sqlite.Microsoft.EntityFrameworkCore.Tools包2.创建模型 数据库上下文模型:Bloggin ...

  2. docker+prom+grafana+altermanager

    docker基础 docker run -it --name centos -v $HOME:/tmp -p 8080:8080 centos docker inspect container #查看 ...

  3. K8S(07)交付实战-架构说明并准备zk集群

    k8s交付实战-架构说明并准备zk集群 目录 k8s交付实战-架构说明并准备zk集群 1 交付的服务架构图: 1.1 架构图解 1.2 交付说明: 2 部署ZK集群 2.1 二进制安装JDK 2.1. ...

  4. 9.[完]其他常用的rabbitmq的参数和设置

    作者 微信:tangy8080 电子邮箱:914661180@qq.com 更新时间:2019-08-12 20:42:25 星期一 欢迎您订阅和分享我的订阅号,订阅号内会不定期分享一些我自己学习过程 ...

  5. 实现基于股票收盘价的时间序列的统计(用Python实现)

    时间序列是按时间顺序的一组真实的数字,比如股票的交易数据.通过分析时间序列,能挖掘出这组序列背后包含的规律,从而有效地预测未来的数据.在这部分里,将讲述基于时间序列的常用统计方法. 1 用rollin ...

  6. MSE,RMSE

    MSE: Mean Squared Error 均方误差是指参数估计值与参数真值之差平方的期望值; MSE可以评价数据的变化程度,MSE的值越小,说明预测模型描述实验数据具有更好的精确度. RMSE  ...

  7. canvas绘制五星红旗

    代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8& ...

  8. java之 javassist简单使用

    0x01.javassist介绍 什么是javassist,这个词一听起来感觉就很懵,对吧~ public void DynGenerateClass() { ClassPool pool = Cla ...

  9. KMP 算法 & 字符串查找算法

    KMP算法 Knuth–Morris–Pratt algorithm 克努斯-莫里斯-普拉特 算法 algorithm kmp_search: input: an array of character ...

  10. what's the print number means after called the setTimeout function in Chrome console?

    what's the print number means after called the setTimeout function in Chrome console? javascript fun ...