一文介绍 Tapable 的特性和使用,助力理解 webpack 插件架构!

一、引言

众所周知,webpack 的 plugins 非常灵活,可以在编译的不同阶段注册事件回调,这个功能便是基于 Tapable 实现的。

Tapable 的使用步骤如下:

  1. 创建钩子实例,如 SyncHookSyncLoopHook 钩子;
  2. 调用订阅接口注册事件回调,包括 taptapAsynctapPromise;
  3. 触发回调,包括 callcallAsyncpromise

Tapable 的钩子分为同步钩子和异步钩子,同步钩子都以 Sync 开头,通过 tap 注册回调,使用 call 触发回调。异步钩子则以 Async 开头,通过 tapAsynccallAsync,或者 tapPromisepromise 来注册和触发回调。

接下来便是各个钩子的介绍和用法。

二、同步钩子

SyncHook

SyncHook 是一个基础的钩子。通过该钩子注册的事件会依次执行。

const { SyncHook } = require('tapable')

// 实例化钩子函数,可以在这里定义形参
const syncHook = new SyncHook(['author', 'age'])

syncHook.intercept({
  // 每次调用 hook 实例的 tap() 方法注册回调函数时, 都会调用该方法,
  // 并且接受 tap 作为参数, 还可以对 tap 进行修改;
  register: (tapInfo) => {
    console.log(`${tapInfo.name} is doing its job`)
    return tapInfo // may return a new tapInfo object
  },
  // 通过hook实例对象上的call方法时候触发拦截器
  call: (arg1, arg2, arg3) => {
    console.log('Starting to calculate routes')
  },
  // 在调用被注册的每一个事件回调之前执行
  tap: (tap) => {
    console.log(tap, 'tap')
  },
  // loop类型钩子中 每个事件回调被调用前触发该拦截器方法
  loop: (...args) => {
    console.log(args, 'loop')
  },
})

// 注册事件
syncHook.tap('监听器1', (name, age) => {
  console.log('监听器1:', name, age)
})

syncHook.tap('监听器2', (name) => {
  console.log('监听器2:', name)
})

syncHook.tap('监听器3', (name, age) => {
  console.log('监听器3:', name, age)
})

// 触发事件,这里传的是实参,会被每一个注册的函数接收到
syncHook.call('wade', '25')

// 执行结果
/**
监听器1 is doing its job
监听器2 is doing its job
监听器3 is doing its job
Starting to calculate routes
{ type: 'sync', fn: [Function (anonymous)], name: '监听器1' } tap
监听器1: wade 25
{ type: 'sync', fn: [Function (anonymous)], name: '监听器2' } tap
监听器2: wade
{ type: 'sync', fn: [Function (anonymous)], name: '监听器3' } tap
监听器3: wade 25
*/

SyncBailHook

SyncBailHook 是一个具有熔断风格的钩子。只要其中一个事件回调有返回值,后面的事件回调就不执行了。

const { SyncBailHook } = require('tapable')

const hook = new SyncBailHook(['author', 'age'])

hook.tap('测试1', (name, age) => {
  console.log('测试1接收参数:', name, age)
})

hook.tap('测试2', (name, age) => {
  console.log('测试2接收参数:', name, age)
  return 'outer'
})

hook.tap('测试3', (name, age) => {
  console.log('测试3接收参数:', name, age)
})

hook.call('wade', '25')

// 执行结果
/**
测试1接收参数: wade 25
测试2接收参数: wade 25
*/

第二个事件返回了 outer,因此第三个事件不会触发。

SyncLoopHook

SyncLoopHook 是一个循环类型的钩子。循环类型的含义是不停的循环执行事件回调,直到所有函数结果 result === undefined,不符合条件就返回从第一个事件回调开始执行。

const { SyncLoopHook } = require('tapable')

const hook = new SyncLoopHook([])

let count = 5

hook.tap('测试1', () => {
  console.log('测试1里面的count:', count)
  if ([1, 2, 3].includes(count)) {
    return undefined
  } else {
    count--
    return '123'
  }
})

hook.tap('测试2', () => {
  console.log('测试2里面的count:', count)
  if ([1, 2].includes(count)) {
    return undefined
  } else {
    count--
    return '123'
  }
})

hook.tap('测试3', () => {
  console.log('测试3里面的count:', count)
  if ([1].includes(count)) {
    return undefined
  } else {
    count--
    return '123'
  }
})

//通过call方法触发事件
hook.call()

// 执行结果
/**
测试1里面的count: 5
测试1里面的count: 4
测试1里面的count: 3
测试2里面的count: 3

测试1里面的count: 2
测试2里面的count: 2
测试3里面的count: 2

测试1里面的count: 1
测试2里面的count: 1
测试3里面的count: 1
*/

SyncWaterfallHook

SyncWaterfallHook 是一个瀑布式类型的钩子。瀑布类型的钩子就是如果前一个事件回调的结果 result !== undefined,则 result 会作为后一个事件回调的第一个参数(也就是上一个函数的执行结果会成为下一个函数的参数)。

const { SyncWaterfallHook } = require('tapable')

const hook = new SyncWaterfallHook(['author', 'age'])

hook.tap('测试1', (name, age) => {
  console.log('测试1接收参数:', name, age)
})

hook.tap('测试2', (name, age) => {
  console.log('测试2接收参数:', name, age)
  return '另外的名字'
})

hook.tap('测试3', (name, age) => {
  console.log('测试3接收参数:', name, age)
})

hook.call('wade', '25')

// 执行结果
/**
测试1接收参数: wade 25
测试2接收参数: wade 25
测试3接收参数: 另外的名字 25
*/

三、异步钩子

异步钩子需要通过 tapAsync 函数注册事件,同时也会多一个 callback 参数,执行 callback 告诉 该注册事件已经执行完成。callback 函数可以传递两个参数** err** 和 result,一旦 err 不为空,则后续的事件回调都不再执行。异步钩子触发事件需要使用 callAsync

AsyncSeriesHook

AsyncSeriesHook 是一个串行的钩子。

const { AsyncSeriesHook } = require('tapable')

const hook = new AsyncSeriesHook(['author', 'age']) // 先实例化,并定义回调函数的形参

console.time('time')

hook.tapAsync('测试1', (param1, param2, callback) => {
  console.log('测试1接收的参数:', param1, param2)
  setTimeout(() => {
    callback()
  }, 1000)
})

hook.tapAsync('测试2', (param1, param2, callback) => {
  console.log('测试2接收的参数:', param1, param2)
  setTimeout(() => {
    callback()
  }, 2000)
})

hook.tapAsync('测试3', (param1, param2, callback) => {
  console.log('测试3接收的参数:', param1, param2)
  setTimeout(() => {
    callback()
  }, 3000)
})

hook.callAsync('wade', '25', (err, result) => {
  // 等全部都完成了才会走到这里来
  console.log('这是成功后的回调', err, result)
  console.timeEnd('time')
})

// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: wade 25
测试3接收的参数: wade 25
这是成功后的回调 undefined undefined
time: 6.032
*/

AsyncSeriesBailHook

AsyncSeriesBailHook 是一个串行、熔断类型的钩子。一旦 callback 函数传递了参数,则不再执行后续的事件回调。

const { AsyncSeriesBailHook } = require('tapable')

const hook = new AsyncSeriesBailHook(['author', 'age'])

console.time('time')

hook.tapAsync('测试1', (param1, param2, callback) => {
  console.log('测试1接收的参数:', param1, param2)
  setTimeout(() => {
    callback()
  }, 1000)
})

hook.tapAsync('测试2', (param1, param2, callback) => {
  console.log('测试2接收的参数:', param1, param2)
  setTimeout(() => {
    callback(false, '123')
  }, 2000)
})

hook.tapAsync('测试3', (param1, param2, callback) => {
  console.log('测试3接收的参数:', param1, param2)
  setTimeout(() => {
    callback()
  }, 3000)
})

hook.callAsync('wade', '25', (err, result) => {
  // 等全部都完成了才会走到这里来
  console.log('这是成功后的回调', err, result)
  console.timeEnd('time')
})

// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: wade 25
这是成功后的回调 null 123
time: 3.013s
*/

AsyncSeriesWaterfallHook

AsyncSeriesWaterfallHook 是一个串行、瀑布式类型的钩子。如果前一个事件回调的 callback 的参数 result !== undefined,则 result 会作为后一个事件回调的第一个参数(也就是上一个函数的执行结果会成为下一个函数的参数)。

const { AsyncSeriesHook } = require('tapable')

const hook = new AsyncSeriesHook(['author', 'age'])

console.time('time')

hook.tapAsync('测试1', (param1, param2, callback) => {
  console.log('测试1接收的参数:', param1, param2)
  setTimeout(() => {
    callback()
  }, 1000)
})

hook.tapAsync('测试2', (param1, param2, callback) => {
  console.log('测试2接收的参数:', param1, param2)
  setTimeout(() => {
    callback(null, 2441)
  }, 2000)
})

hook.tapAsync('测试3', (param1, param2, callback) => {
  console.log('测试3接收的参数:', param1, param2)
  setTimeout(() => {
    callback()
  }, 3000)
})

hook.callAsync('wade', '25', (err, result) => {
  // 等全部都完成了才会走到这里来
  console.log('这是成功后的回调', err, result)
  console.timeEnd('time')
})

// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: 新名字A 25
测试3接收的参数: 新名字A 25
这是成功后的回调 null 新名字C
time: 6.043s
*/

AsyncParallelHook

AsyncParallelHook 是一个并行的钩子。通过这个钩子注册的回调不需要等待上一个事件回调执行结束便可以执行下一个事件回调。

const { AsyncParallelHook } = require('tapable')

const hook = new AsyncParallelHook(['author', 'age'])

console.time('time')

hook.tapAsync('测试1', (param1, param2, callback) => {
  setTimeout(() => {
    console.log('测试1接收的参数:', param1, param2)
    callback()
  }, 2000)
})

hook.tapAsync('测试2', (param1, param2, callback) => {
  console.log('测试2接收的参数:', param1, param2)
  callback()
})

hook.tapAsync('测试3', (param1, param2, callback) => {
  console.log('测试3接收的参数:', param1, param2)
  callback()
})

// call 方法只有同步钩子才有,异步钩子得使用 callAsync
hook.callAsync('wade', '25', (err, result) => {
  // 等全部都完成了才会走到这里来
  console.log('这是成功后的回调', err, result)
  console.timeEnd('time')
})

// 执行结果
/**
测试2接收的参数: wade 25
测试3接收的参数: wade 25
测试1接收的参数: wade 25
这是成功后的回调 undefined undefined
time: 2.015s
*/

第一个事件设置了 2 秒的定时器再执行,此时会先执行后续的事件回调,等到 2 秒后再执行第一个事件回调。

AsyncParallelBailHook

AsyncParallelBailHook 是一个串行、熔断类型的钩子。若当前的事件回调的 callback 的参数 result !== undefined,则后续的事件都不再执行。

const { AsyncParallelBailHook } = require('tapable')

const hook = new AsyncParallelBailHook(['author', 'age'])

console.time('time')

hook.tapAsync('测试1', (param1, param2, callback) => {
  console.log('测试1接收的参数:', param1, param2)
  setTimeout(() => {
    callback()
  }, 1000)
})

hook.tapAsync('测试2', (param1, param2, callback) => {
  console.log('测试2接收的参数:', param1, param2)
  callback(null, '测试2有返回值啦')
})

hook.tapAsync('测试3', (param1, param2, callback) => {
  console.log('测试3接收的参数:', param1, param2)
  setTimeout(() => {
    callback(null, '测试3有返回值啦')
  }, 3000)
})

hook.callAsync('wade', '25', (err, result) => {
  // 等全部都完成了才会走到这里来
  console.log('这是成功后的回调', err, result)
  console.timeEnd('time')
})

// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: wade 25
这是成功后的回调 null 测试2有返回值啦
time: 3.015s
*/

由于第二个事件回调有返回值,因此第三个事件回调不会执行。若第一个事件回调也有返回值,尽管是第二个事件回调先执行完成,但是 callAsync 拿到的结果依然是第一个事件回调的返回值,示例如下:

const { AsyncParallelBailHook } = require('tapable')

const hook = new AsyncParallelBailHook(['author', 'age'])

console.time('time')

hook.tapAsync('测试1', (param1, param2, callback) => {
  console.log('测试1接收的参数:', param1, param2)
  setTimeout(() => {
    callback(null, '测试1有返回值啦')
  }, 3000)
})

hook.tapAsync('测试2', (param1, param2, callback) => {
  console.log('测试2接收的参数:', param1, param2)
  callback(null, '测试2有返回值啦')
})

hook.tapAsync('测试3', (param1, param2, callback) => {
  console.log('测试3接收的参数:', param1, param2)
  setTimeout(() => {
    callback(null, '测试3有返回值啦')
  }, 1000)
})

hook.callAsync('wade', '25', (err, result) => {
  // 等全部都完成了才会走到这里来
  console.log('这是成功后的回调', err, result)
  console.timeEnd('time')
})

// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: wade 25
这是成功后的回调 null 测试1有返回值啦
time: 3.009s
*/

四、总结

Tapable 提供的这些钩子,支持同步、异步、熔断、循环、waterfall 等功能特性,以此支撑起 webpack 复杂的编译功能,在理解这些内容之后,我们对 webpack 插件架构的设计会有进一步的理解和使用。

参考资料

https://juejin.cn/post/6955421936373465118
https://github.com/webpack/tapable

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/744632.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

报道 | 2024年7月-2024年9月国际运筹优化会议汇总

封面图来源: https://www.pexels.com/zh-cn/photo/1181406/ 2024年7月-2024年9月召开会议汇总: 2024 INFORMS Advances in Decision Analysis Conference (ADA) Location: Finland Important Dates: Conference: July 10-12, 2024 Details:https://w…

【学习】科大睿智解读ITSS认证中咨询机构的作用

企业拥有ITSS认证这不仅将为企业开拓商机,提升竞争力,还能促使企业改进内部运维流程,提高服务质量,为客户提供更优质的IT运维支持。在ITSS认证中,咨询机构扮演着重要的角色,其主要作用包括以下几个方面&…

【服务器】磁盘满载--docker 的日志文件太大造成满载

一.背景 早上过来测试反馈服务器都宕机了,访问不了。一看服务器磁盘都已经满了。所以开始清磁盘数据。 二.解决 主要查看下面目录情况: /home/libe/docker /containers /volumes /overlay21.查看磁盘情况 df -h/ du -a|sort -rn|…

前端开发的工厂设计模式

在前端开发中,工厂设计模式(Factory Pattern)是一种非常有用的设计模式,能够帮助我们在创建对象时减少代码的重复性和复杂性。 一、工厂设计模式概述 工厂设计模式是一种创建型设计模式,主要目的是定义一个用于创建对…

【PL理论深化】(6) Ocaml 语言: 函数 | 匿名函数 | 函数可以接受多个参数 | OCaml 是一种将函数视为 first-class 的语言

💬 写在前面:本章我们继续介绍如何使用 OCaml 进行函数式编程。介绍如何使用 let 定义函数,讲解匿名函数,函数可以接受多个参数,以及讨论 OCaml 将函数视为 first-class。 目录 0x00 函数 0x01 匿名函数&#xff08…

【C语言】--常见类型和概念

❤️个人主页: 起名字真南 &#x1f495;个人专栏:【数据结构初阶】 【C语言】 目录 第一个C语言程序main函数printf函数库函数关键字字符和ASCII码字符串和\0转义字符 第一个C语言程序 #include<stdio.h> int main() {printf("Hello World\n");return 0; }ma…

【神经网络】CNN网络:深入理解卷积神经网络

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步&#xff01; CNN网络&#xff1a;深入理解…

springboot集成达梦数据库,打包后,tomcat中启动报错

背景&#xff1a;springboot集成达梦数据库8&#xff0c;在工具idea中正常使用&#xff0c;但是打包后&#xff0c;无法启动&#xff0c;报错 pom引入的依赖 但是这种情况&#xff0c;只有在idea中启动没问题的解决方法 需要修改引入的依赖&#xff0c;再次打包就可以 <d…

PatchMixer:一种用于长时间序列预测的Patch混合架构

前言 《PatchMixer: A Patch-Mixing Architecture for Long-Term Time Series Forecasting》原文地址&#xff0c;Github开源代码地址GitHub项目地址Some-Paper-CN。本项目是译者在学习长时间序列预测、CV、NLP和机器学习过程中精读的一些论文&#xff0c;并对其进行了中文翻译…

苹果电脑压缩pdf文件,苹果电脑里如何压缩pdf文件

压缩PDF文件是现代办公和日常生活中经常需要处理的一项任务&#xff0c;无论是为了节省存储空间、方便网络传输&#xff0c;还是为了在移动设备上更流畅地阅读文档&#xff0c;学会有效地压缩PDF都显得尤为重要。在本文中&#xff0c;我们将详细探讨压缩PDF的方法&#xff0c;从…

Mac安装多版本node

Mac下使用n模块去安装多个指定版本的Node.js&#xff0c;并使用命令随时切换。 node中的n模块是&#xff0c;node专门用来管理node版本的模块&#xff0c;可以进行node版本的切换&#xff0c;下载&#xff0c;安装。 1.安装n npm install -g n 2.查看版本 n --version 3.展…

LeetCode题练习与总结:随机链表的复制--138

一、题目描述 给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成&#xff0c;其中每个新节点的值都设为其对应的原节点的…

IT入门知识第八部分《云计算》(8/10)

目录 云计算&#xff1a;现代技术的新篇章 1. 云计算基础 1.1 云计算的起源和发展 云计算的早期概念 云计算的发展历程 1.2 云计算的核心特点 按需自助服务 广泛的网络访问 资源池化 快速弹性 按使用量付费 1.3 云计算的优势和挑战 成本效益 灵活性和可扩展性 维…

日语培训日语等级考试柯桥小语种学习语言学校

什么是外来语 外来语是指在日本的国语中使用的来源于外国语言的词汇。但狭义上的外来语则是指来源于欧美国家语言的词汇&#xff0c;其中大部分是来源于英美语系的词汇。日语中的汉语词汇很多&#xff0c;大多是自古以来从中国引进的&#xff0c;从外来语的定义看&#xff0c;汉…

运算符重载详解(完全版)

1.运算符重载 C为了增强代码的可读性引入了运算符重载&#xff0c;运算符重载是具有特殊函数名的函数&#xff0c;也具有其返回值类型&#xff0c;函数名字和参数列表&#xff0c;其返回值类型与参数列表都与普通的函数类似 函数名&#xff1a;关键字operator后面接需要重载的…

记因hive配置文件参数运用不当导致 sqoop MySQL导入数据到hive 失败的案例

sqoop MySQL导入数据到hive报错 ERROR tool.ImportTool: Encountered IOException running import job: java.io.IOException: Hive exited with status 64 报错解释&#xff1a; 这个错误表明Sqoop在尝试导入数据到Hive时遇到了问题&#xff0c;导致Hive进程异常退出。状态码…

Lua网站开发之文件表单上传

这个代码示例演示如何上传文件或图片&#xff0c;获取上传信息及保存文件到本地。 local fw require("fastweb") local request require("fastweb.request") local response require("fastweb.response") local cjson require("cjson&q…

windterm多窗口同时操作多台服务器

在配置k8s等多服务器环境时&#xff0c;我们要对多台服务器进行相同的操作&#xff0c;使用多窗口同步输入实现一次命令多段执行 初始配置&#xff0c;服务器都已连好 分屏窗口&#xff0c;按下altw->alth水平分屏&#xff0c;按下altw->altv,垂直分屏 按下ctrlshiftm,…

SAP消息号 VF028

客户在VF11冲销发票之后&#xff0c;没有生成正式的财务凭证&#xff0c;然后VF02的时候出现如下报错&#xff1a; “自动清算出具发票凭证XXXXXXX&#xff08;被冲销凭证号&#xff09;且不可能取消凭证XXXXXXX&#xff08;冲销凭证号&#xff09; 原因&#xff1a;销售订单2…

小程序下拉刷新,加载更多数据,移动端分页

文章目录 页面结构图WXML页面代码js代码wxss代码总结备注 参考&#xff1a;https://juejin.cn/post/7222855604406796346 页面结构图 一般页面就4个结构&#xff1a;最外滚动层、数据展示层、暂无数据层、没有更多数据层。 如图&#xff1a; WXML页面代码 <scroll-view …