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 简单版的实现
上面的代码,我们可以继续优化:把每个流程点都抽象成一个独立插件,最后再串联起来。如处理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
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)