写点什么

Webpack 的插件机制 - Tapable

  • 2021 年 7 月 31 日
  • 本文字数:3719 字

    阅读完需:约 12 分钟

Webpack 的插件机制 - Tapable

前言


用了这么久的 Webpack,你一定对它的生态重要组成部分loaderplugin很好奇吧,你是否尝试过编写自己的插件呢,是否了解过 Webpack 的插件机制呢,什么?没有,那还不赶紧上车学一波!


1、tapable


Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。Webpack 通过 Tapable 来组织这条复杂的生产线。Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。——「深入浅出 Webpack」


作为 Webpack 的核心库,tabpable承包了 Webpack 最重要的事件工作机制,包括 Webpack 源码中高频的两大对象(compilercompilation)都是继承自Tapable类的对象,这些对象都拥有Tapable的注册和调用插件的功能,并向外暴露出各自的执行顺序以及hook类型,详情可见文档


2、tapable 的钩子


const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
复制代码


上面是官方文档给出的 9 种钩子的类型,我们看命名就能大致推测他们的类型和区别,分成同步、异步,瀑布流、串行、并行类型、循环类型等等,钩子的目的是为了显式地声明,触发监听事件时(call)传入的参数,以及订阅该钩子的 callback 函数所接受到的参数,举个最简单的🌰


const sync = new SyncHook(['arg']) // 'arg' 为参数占位符sync.tap('Test', (arg1, arg2) => {  console.log(arg1, arg2) // a,undefined})sync.call('a', '2')
复制代码


上述代码定义了一个同步串行钩子,并声明了接收的参数的个数,可以通过hook实例对象(SyncHook本身也是继承自Hook类的)的tap方法订阅事件,然后利用call函数触发订阅事件,执行 callback 函数,值得注意的是 call 传入参数的数量需要与实例化时传递给钩子类构造函数的数组长度保持一致,否则,即使传入了多个,也只能接收到实例化时定义的参数个数。


序号钩子名称执行方式使用要点
1SyncHook同步串行不关心监听函数的返回值
2SyncBailHook同步串行只要监听函数中有一个函数的返回值不为 null,则跳过剩余逻辑
3SyncWaterfallHook同步串行上一个监听函数的返回值将作为参数传递给下一个监听函数
4SyncLoopHook同步串行当监听函数被触发的时候,如果该监听函数返回 true 时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
5AsyncParallelHook异步并行不关心监听函数的返回值
6AsyncParallelBailHook异步并行只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到 callAsync 等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
7AsyncSeriesHook异步串行不关心 callback()的参数
8AsyncSeriesBailHook异步串行callback()的参数不为 null,就会直接执行 callAsync 等触发函数绑定的回调函数
9AsyncSeriesWaterfallHook异步串行上一个监听函数的中的 callback(err, data)的第二个参数,可以作为下一个监听函数的参数

上述表格罗列了所有 hook 的使用方式和要点。


3、注册事件回调


注册事件回调有三个方法:taptapAsync 和 tapPromise,其中 tapAsync 和 tapPromise 不能用于 Sync 开头的钩子类,强行使用会报错。tap的使用方式在上文已经展示过了,就用官方文档的例子展示下tapAsync的使用方式,相比于taptapAsync需要执行 callback 函数才能确保流程会走到下一个插件中去。


myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin", (source, target, routesList, callback) => { bing.findRoute(source, target, (err, route) => {  if(err) return callback(err);  routesList.add(route);  // call the callback  callback(); });});
复制代码


4、触发事件


触发事件的三个方法是与注册事件回调的方法一一对应的,这点从方法的名字上也能看出来:call 对应 tapcallAsync 对应 tapAsync 和 promise 对应 tapPromise。一般来说,我们注册事件回调时用了什么方法,触发时最好也使用对应的方法。同样需要注意的是 callAsync 有个 callback 函数,在逻辑完毕时需要执行,一些具体用法类似于上面的注册事件类似,就不一一展开了。


5、了解机制


那么在 Webpack 中到底如何使用 tapable 调用这些 plugin 呢?


我们首先来看官网给出的编写一个 plugin 的示例


class HelloWorldPlugin {  apply(compiler) {    compiler.hooks.done.tap('Hello World Plugin', (      compilation /* compilation is passed as an argument when done hook is tapped.  */    ) => {      console.log('Hello World!');    });  }}
module.exports = HelloWorldPlugin;
复制代码


上述代码块编写了一个叫 HelloWorldPlugin 的类,它提供了一个叫apply的方法,在该方法中我们可以从外部获取到 Webpack 执行全过程中单一的compiler实例,通过compiler实例,我们可以在 Webpack 的生命周期的done节点(也就是上面我们提到的hook)tap 一个监听事件,也就是说当 Webpack 全部流程执行完毕时,监听事件将会被触发,同时stat统计信息会被传入到监听事件中,在事件中,我们就可以通过stat做一系列我们想要做的数据分析。一般来说,使用一个 Webpack 插件,需要在 Webpack 配置文件中导入(import)插件的类,new 一个实例,like this:


// Webpack.config.jsvar HelloWorldPlugin = require('hello-world');
module.exports = { // ... configuration settings here ... plugins: [new HelloWorldPlugin({ options: true })]};
复制代码


这里聪明的你一定想到了 Webpack 应该是读取了这份配置文件后获得了HelloWorldPlugin实例,并调用了实例的apply方法,在done节点上添加了监听事件!没错,让我们来追溯下 Webpack 的源码部分,在 Webpack 项目的lib/Webpack.js文件中,我们可以看到


if (options.plugins && Array.isArray(options.plugins)) {    for (const plugin of options.plugins) {  if (typeof plugin === "function") {   plugin.call(compiler, compiler);  } else {   plugin.apply(compiler);  } }}
复制代码


这段代码中options就是指配置文件导出的整个对象,这里可以看到 Webpack 循环遍历了一遍 plugins,并分别调用了他们的 apply 方法,当然如果 plugin 是function类型,就直接用call来执行,这也就是我上文提到的一般来说的例外,如果你的插件逻辑很简单,你可以直接在配置文件里写一个function,去执行你的逻辑,而不必啰嗦的写一个类或者用更纯粹的prototype去定义类的方法。到这里为止,我们已经了解了插件中的监听事件是如何注册到 Webpack 的compilecompilationtapable类)上去的,那监听事件是如何、何时被触发的呢,理论上应该是先注册完毕,后触发,这样监听事件才有意义,我们接着发现,在lib/Compiler.js中的Compiler类的run函数里有这样一段代码


const onCompiled = (err, compilation) => { if (err) return finalCallback(err);
if (this.hooks.shouldEmit.call(compilation) === false) { ... this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); return; }
this.emitAssets(compilation, err => { if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) { ... this.hooks.done.callAsync(stats, err => { ... }); return; }
this.emitRecords(err => { if (err) return finalCallback(err);
... this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); }); });};... this.compile(onCompiled);
复制代码


回调函数onCompiled会在compile过程结束时被调用,无论走到哪个 if 逻辑中,this.hooks.done.callAsync都会被执行,也就是说在 done 节点上注册的监听事件会按照顺序依次被触发执行。接着我们再向上追溯,包裹了onCompiled函数的run函数是在lib/Webpack.js中被执行的


if (Array.isArray(options)) {    ...} else if (typeof options === "object") {    ... compiler = new Compiler(options.context); compiler.options = options; if (options.plugins && Array.isArray(options.plugins)) {  for (const plugin of options.plugins) {   if (typeof plugin === "function") {    plugin.call(compiler, compiler);   } else {    plugin.apply(compiler);   }  } }} else { ...}if (callback) { ... compiler.run(callback);}
复制代码


刚好在plugin.apply()的后面,所以是符合先注册监听事件,再触发的逻辑顺序的。


是不是已经有点乱了,来来来,我们用流程图简单捋一下。

插件的注册执行流程图示



6、总结


tapable 作为 Webpack 的核心库,承接了 Webpack 最重要的事件流的运转,它巧妙的钩子设计很好的将实现与流程解耦开来,真正实现了插拔式的功能模块,在 Webpack 中最核心的负责编译的 Compiler 和负责创建的 bundles 的 Compilation 都是 Tapable 的实例,可以说想要真正读懂 Webpack,tapable 的知识储备是必不可少的,它的一些设计思想也是很值得我们借鉴的,本文只是对 tapable 的一些 api 以及 Webpack 如何使用 tapable 串起了整个插件流工作机制做了介绍。



头图:Unsplash

作者:丁楠

原文:https://mp.weixin.qq.com/s/qWq46-7EJb0Byo1H3SDHCg

原文:Webpack 的插件机制 - Tapable

来源:微医大前端技术 - 微信公众号 [ID:wed_fed]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021 年 7 月 31 日 22:002605

评论

发布
暂无评论
发现更多内容

docker 安装consul

Shae

Python中的with是测试常用到的资源打开利器

陈磊@Criss

LeetCode题解:1. 两数之和,Map+队列+双指针,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

理财专题一

TCA

TNFE-Weekly[第七十二周已更新]

莹姐🙈

小程序 前端 周刊

随想之乐观估计

云杉

5分钟带你掌握Makefile分析

华为云开发者社区

makefile 脚本

记一次node项目重构改进

华为云开发者社区

Java 项目 方案

测试

云龙

解决分布式session问题

架构师修行之路

分布式 架构设计 session

SwiftGG 文档翻译笔记1-基础部分函数闭包

实践案例丨基于Raft协议的分布式数据库系统应用

华为云开发者社区

raft 华为云

USDT承兑商支付系统搭建,区块链支付平台开发

13823153121

程序员快乐器之JAVA代码生成工具

Philips

敏捷开发 程序设计 软件架构 开发工具

区块链永续合约交易所搭建,永续合约系统开发

13823153121

中国云计算的云栖“坐标”

脑极体

组合模式

纯纯

C/C++基础之sizeof使用

C语言与CPP编程

c c++ 编程 编程语言

你需要开始做点什么,否则你会一直忙一直忙

老胡爱分享

学习 思维方式 行动派 随笔杂谈 拖延症

测试

Http请求中如何保持状态?

架构师修行之路

使用Valgrind调试Linux C++程序

Simon

c++ gdb Valgrind memcheck 内存泄漏

案例解析丨Spark Hive自定义函数应用

华为云开发者社区

spark UDF

直播风口,是什么在支撑教育、电商、泛娱乐等场景?

腾讯云音视频

腾讯云 音视频 云直播 点播

2020英特尔大师挑战赛携手华硕ROG激战成都

新闻科技资讯

中途转行学Java,如何赛过科班生?你只需要做到这几点

小Q

Java 程序员 架构 技术 基础

大项目写代码写到晕头转向?敏捷多项目框架解君愁

Learun

敏捷开发 软件开发

什么是云服务器,ECS它能干什么?

德胜网络-阳

【写作群星榜】9.1~9.13写作平台优秀作者 & 文章排名

InfoQ写作平台官方

写作平台 排行榜 文章

你还在手撕微服务?快试试 go-zero 的微服务自动生成

万俊峰Kevin

微服务 microservice go-zero Go 语言

架构师课程大作业 知识图谱

杉松壁

Flutter 自动化测试

Flutter 自动化测试

Webpack 的插件机制 - Tapable-InfoQ