手把手教你学习webapck插件plugin的开发

本文深入解析webpack插件的工作原理和开发,包括Tapable钩子、Compiler和Compilation对象,以及如何注册和使用各种钩子,如environment、emit、make等。通过示例,介绍了插件基础结构、异步钩子执行顺序和调试技巧,帮助读者掌握自定义webpack插件的开发。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

准备知识

作用

通过插件我们可以拓展webpack,加入自定义的构建行为,使webpack可以执行更广泛的任务,拥有更强的构建能力。

工作原理

webpack在编译过程中,会触发一些列Tapable钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件。这样,当webpack构建的时候,插件注册的事件就会随着钩子的触发而执行了。

webpack内部的钩子

什么是钩子

钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack把编译过程中触发的各类关键事件封装成了事件接口暴露了出来。这些接口被很形象的称作:hooks(钩子)。开发插件,离不开这些钩子。

Tapble

Tapble为webpack提供了统一的接口(钩子)类型定义,他是webpack核心功能库,webpack中目前有很多中hooks,在Tapble源码中可以看到,他们是:

github.com/webpack/tap…

exports.__esModule = true;
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook"); 

Tapble还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

  • tap:可以注册同步和异步钩子
  • tapAsync:回调方式注册异步钩子
  • tapPromise:Promise方式注册异步钩子

Plugin构建对象

Compiler

compiler对象中保存着完整的webpack环境配置,每次启动webpack构建时它都是一个独一无二,仅仅创建一次的对象。这个对象会在首次启动webpack时构建,我们可以通过compiler对象访问到webpack的主环境配置,如loader、plugin等配置信息。

compiler主要有以下属性:

属性名
options可以访问本次启动的webpack时的所有配置文件,如loaders、entry、output、plugin等等配置信息。
inputFileSystem可以进行文件操作,功能如Node.js内的fs
outputFileSystem可以进行文件操作,功能如Node.js内的fs
hooks可以注册Tabable的不同种类Hook,从而可以在compiler生命周期中植入不同的逻辑

Compilation

compilation对象代表一次资源的构建,compilation实例能够访问所有的模块和它们的依赖

一个compilation对象会对构建依赖图中所有模块,进行编译。在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、分块(chunk)、哈希(hash)和重新创建(restore)

它主要有以下属性

属性名
modules可以访问所有模块,打包的每一个文件都是一个模块。
chunkschunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。
assets可以访问本次打包生成所有文件的结果。
hooks可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。

生命周期简图

插件开发基础示例

插件基础结构示例

一个插件由以下构成

  • 一个具名 JavaScript 函数。
  • 在它的原型上定义 apply 方法。
  • 指定一个触及到 webpack 本身的 事件钩子
  • 操作 webpack 内部的实例特定数据。
  • 在实现功能后调用 webpack 提供的 callback。
// 一个 JavaScript class
class MyExampleWebpackPlugin {// 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数apply(compiler) {// 指定要附加到的事件钩子函数compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin',(compilation, callback) => {......callback();});}
} 

手写一个最简单的插件

创建一个test-plugin.js的文件

class TestPlugin {constructor() {console.log("我的第一个插件");}apply(){}
}

module.exports = TestPlugin; 

webpack.config.js中引入并使用

const path = require("path");
const TestPlugin = require("./plugins/test-plugin");

module.exports = {entry: "./src/main.js",output: {path: path.resolve(__dirname, "./dist"),filename: "js/[name].js",},module: {rules: [],},plugins: [new TestPlugin(),],mode: "production",
}; 

命令行执行 webpack,可以看到插件正常执行了。

2.webpack创建compiler对象3.遍历所有plugins中插件,调用插件的apply方法4.执行剩下编译流程(触发各个hooks事件)注:如果没有写apply函数,webpack会报错:Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.

注册hooks

钩子函数:v4.webpack.docschina.org/api/compile…

钩子函数的调用,采用下面方法。

这个方法,接受两个参数,第一个参数是当前定义的类名,第二个参数是回调函数,如

// 一个 JavaScript class
class MyExampleWebpackPlugin {// 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数apply(compiler) {// 指定要附加到的事件钩子函数compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin',(compilation, callback) => {......callback();});}
} 

注,回调函数的参数需要查看对应的钩子函数需不需要参数,如果没有写,代表没有参数。如:

如果有,回调函数的参数就是文档上写的,如:

environment钩子

根据environment钩子,我们来在插件内增加一些逻辑

class TestPlugin {constructor() {console.log("---------------------------------------------------------------");}apply(compiler) {// 由文档可知,environment是同步钩子,所以需要使用tap注册compiler.hooks.environment.tap("TestPlugin", () => {console.log("我是environment钩子");});}
}

module.exports = TestPlugin; 

执行打包命令

可以发现,这个钩子会自动执行。

我们在学习几个常用的钩子

emit钩子

钩子类型执行时机参数
AsyncSeriesHook(异步串行钩子)生成资源到 output 目录之前。compilation

根据这个钩子,我们体验下三种调用方式的异同。

class TestPlugin {constructor() {console.log("-------------------------------------------------------------------------------");}apply(compiler) {// 由文档可知,environment是同步钩子,所以需要使用tap注册compiler.hooks.environment.tap("TestPlugin", () => {console.log("我是environment钩子");});// emitA:由文档可知,emit是异步串行钩子 AsyncSeriesHookcompiler.hooks.emit.tap("TestPlugin", (compilation) => {console.log("emit AAAAAAAAA");});// emitB:compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {setTimeout(() => {console.log("emit BBBBBBBBB");// 使用tapAsync的调用方式,必须返回callback()代表结束callback();}, 2000);});// emitC:compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => {return new Promise((resolve) => {setTimeout(() => {console.log("emit CCCCCCCCC");// tapPromise的方式通过resolve() 或 reject() 结束函数resolve();}, 1000);});});}
}

module.exports = TestPlugin; 

通过示例,我们可以发现三种函数的写法是不一样,这是我们要注意的。然后,我们可以思考一下,代码执行后的打印顺序。

你可能有一个地方比较疑惑,emit是异步钩子,为什么还可以通过tap来注册我们的插件?

异步钩子可以通过tap方法来注册插件,但是回调函数内部只能写同步代码,不能写异步代码。

上述代码执行的顺序是:

通过这个示例,我们可以得出这样的结论

  • apply内的钩子函数是从上往下依次执行的
  • 执行过程中,遇到异步任务,会等异步任务执行完毕继续向下执行
  • 三种函数写法是不一样的

我们在看看异步并行钩子的执行顺序

make钩子

钩子类型执行时机参数
AsyncParallelHook(异步串行钩子)compilation
/*1. webpack加载webpack.config.js中所有配置,此时就会new TestPlugin(), 执行插件的constructor2. webpack创建compiler对象3. 遍历所有plugins中插件,调用插件的apply方法4. 执行剩下编译流程(触发各个hooks事件)
*/
class TestPlugin {constructor() {console.log("-------------------------------------------------------------------------------");}apply(compiler) {......// 由文档可知,make是异步并行钩子 AsyncParallelHookcompiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {// 需要在compilation hooks触发前注册才能使用setTimeout(() => {console.log("TestPlugin make 111");callback();}, 3000);});compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {setTimeout(() => {console.log("TestPlugin make 222");callback();}, 1000);});compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {setTimeout(() => {console.log("TestPlugin make 333");callback();}, 2000);});}
}

module.exports = TestPlugin; 

根据执行结果,我们可以看出,对于异步并行钩子,是谁快谁先结束。

注:每个钩子也有自己优先级,比如make的优先级是高于emit的

compilation 钩子

compilation作为compiler钩子的回调函数参数,它也是有钩子函数的,用法是compiler钩子一直的。

使用node调试代码

我们如果想看compilation或compiler里面具体有什么内容,在命令行打印基本上是看不来东西的,实在太乱太多了!!!!我们可以借助node调试的方式进行查看。

首先,我们在项目内打个断点

class TestPlugin {constructor() {console.log("-------------------------------------------------------------------------------");}apply(compiler) {debuggerconsole.log("compiler");// emitA:由文档可知,emit是异步串行钩子 AsyncSeriesHookcompiler.hooks.emit.tap("TestPlugin", (compilation) => {console.log("compilation");});}
}

module.exports = TestPlugin; 

我们在package.json中增加一段node代码

"scripts": {"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
} 
  • – insepct 是node内置的调试指令(借助浏览器控制台实现调试)
  • -brk 指在代码的首个位置打一个断点
  • ./node_modules/webpack-cli/bin/cli.js 指webpack打包命令一旦执行时就进入调试状态

我们在命令行执行 npm run debug

然后打开谷歌浏览器任意一个界面,进入控制台,稍等片刻,会发现出现一个node标志

点进去,就可以调试了!第一个断点是-brk生效了,这是webpack cli.js 的第一句代码

我们点击继续执行脚本,然后就可以调试了

现在,可以方便的看compiler里面到底是什么东西了!

## 最后 整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。 ![](https://img-blog.csdnimg.cn/8326170d994a40c38c9c982807b5c454.jpeg#pic_center) **有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享**

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值