【Webpack Plugin】写了个插件跟喜欢的女生表白,结果......😭😭😭

15,660 阅读12分钟

👋 事情是这样的

作为一名母胎 solo 二十几年的我,平平无奇的一直活在别人的狗粮之下。渐渐的,我好像活成了一个随时见证别人爱情,也随时能为失恋的人排忧解难的角色。

image.png

直到前两天,公司新来了一个前端妹子。

相视的第一眼,我神迷了,我知道,终究是躲不过去了......

image.png

相逢却似曾相识,未曾相识已相思!

当晚,彻夜未眠...

6839f22e-2f0c-4117-b02b-5db21822e8f9.jpg

第二天早上,从同事的口中得知了女生的名字,我们暂且叫她小舒吧。

为了不暴露我的狼子野心(欲擒故纵拿捏的死死的),我决定出于同事的关心询问一下项目了解的怎么样了,有没有需要我帮忙的。

没想到小舒像抓到了救命稻草一样:“小哥,你来的正好,过来帮我看看项目怎么跑不起来??”

8f8a7944-af3a-41a3-9d46-0828aade3146.jpg

我回到座位上,很快的发现是由于项目中部分包的版本不兼容导致的,更新下版本就可以了。

正准备起身去找小舒时,一个奇怪的念头闪过......

我决定给我们的第一次交流一个惊喜:借着这次解决问题的机会,好好拉近一下我们之间的关系!!!

10145af4-9407-40a8-ba24-8b64bfebeaa8.jpg

想法一来便挡也挡不住。我决定在项目中运行一个插件:当启动项目时,直接在控制台中向小舒表达我的心意!!!😏😏😏

没办法,单身这么多年肯定是有原因的!一定是我不够主动!这次我可要好好把握这个机会!!!

f689092c-a552-4fda-8189-d6c7f59fecd3.jpg

🏂 说干就干

有了想法就开干,哥从来不是一个拖拖拉拉的人。

小舒的项目用的是 Webpack + React 技术栈,既然想要在项目启动的时候做事情,那肯定是得写个 Webpack 插件了。

先去官网了解一下 Webpack Plugin 的概念:

Webpack Plugin:向第三方开发者提供了 Webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以在 Webpack 构建流程中引入自定义的行为。创建插件比创建 loader 更加高级,因为你需要理解 Webpack 底层的特性来处理相应的钩子

867997b1-26f7-40fb-b4b2-c911306bdda4.jpg

通俗点说就是可以在构建流程中插入我们的自定义的行为,至于在哪个阶段插入或者做什么事情都可以通过 Webpack Plugin 来完成。

另外官网还提到,想要弄清楚 Webpack 插件 得先弄清楚这三个东西:tapablecompilercompilation对象,先快点花几分钟去了解一下,争取在中午吃饭前搞定!

27df2766-960f-4c9c-823c-46d62092bdd9.jpg

💁 tapable的使用姿势

tapable是一个类似于 Node.js 中的 EventEmitter 的库,但它更专注于自定义事件的触发和处理。通过 tapable 我们可以注册自定义事件,然后在适当的时机去触发执行。

e9bb13dc-a5b9-4e89-a258-8001c80d7558.jpg

举个例子🌰:类比到 VueReact 框架中的生命周期函数,它们就是到了固定的时间节点就执行对应的生命周期,tapable 做的事情就和这个差不多,可以先注册一系列的生命周期函数,然后在合适的时间点执行。

概念了解的差不多了,接下来去实操一下。初始化项目,安装依赖:

npm init //初始化项目
yarn add tapable -D //安装依赖

安装完项目依赖后,根据以下目录结构来添加对应的目录和文件:

├── dist # 打包输出目录
├── node_modules
├── package-lock.json
├── package.json
└── src # 源码目录
     └── index.js # 入口文件

根据官方介绍,tapable 使用起来还是挺简单的,只需三步:

  1. 实例化钩子函数( tapable会暴露出各种各样的 hook,这里以同步钩子Synchook为例)
  2. 注册事件
  3. 触发事件

src/index.js

const { SyncHook } = require("tapable"); //这是一个同步钩子

//第一步:实例化钩子函数,可以在这里定义形参
const syncHook = new SyncHook(["author"]);

//第二步:注册事件1
syncHook.tap("监听器1", (name) => {
  console.log("监听器1:", name);
});

//第二步:注册事件2
syncHook.tap("监听器2", (name) => {
  console.log("监听器2", name);
});

//第三步:触发事件
syncHook.call("不要秃头啊");

运行 node ./src/index.js,拿到执行结果:

监听器1 不要秃头啊
监听器2 不要秃头啊

63c7e8b4-11bd-4cc5-a96d-be8bcc486365.jpg

从上面的例子中可以看出 tapable 采用的是发布订阅模式通过 tap 函数注册监听函数,然后通过 call 函数按顺序执行之前注册的函数

大致原理:

class SyncHook {
  constructor() {
    this.taps = [];
  }

  //注册监听函数,这里的name其实没啥用
  tap(name, fn) {
    this.taps.push({ name, fn });
  }

  //执行函数
  call(...args) {
    this.taps.forEach((tap) => tap.fn(...args));
  }
}

另外,tapable 中不仅有 Synchook,还有其他类型的 hook:

image.png

image.png

这里详细说一下这几个类型的概念:

  • Basic(基本的):执行每一个事件函数,不关心函数的返回值
  • Waterfall(瀑布式的):如果前一个事件函数的结果 result !== undefined,则 result 会作为后一个事件函数的第一个参数(也就是上一个函数的执行结果会成为下一个函数的参数)
  • Bail(保险的):执行每一个事件函数,遇到第一个结果 result !== undefined 则返回,不再继续执行(也就是只要其中一个有返回了,后面的就不执行了)
  • Loop(循环的):不停的循环执行事件函数,直到所有函数结果 result === undefined

大家也不用死记硬背,遇到相关的需求时查文档就好了。

在上面的例子中我们用的SyncHook,它就是一个同步的钩子。又因为并不关心返回值,所以也算是一个基本类型的 hook。

0564085f-3d25-4be0-aeed-6e24c6205762.jpg

👫 tabpable 和 Webpack 的关系

要说它们俩的关系,可真有点像男女朋友之间的难舍难分......

5be98b8a-f500-4f28-8240-a69e567e71b1.jpg

Webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,比如

  • 在打包前需要处理用户传过来的参数,判断是采用单入口还是多入口打包,就是通过 EntryOptionPlugin 插件来做的
  • 在打包过程中,需要知道采用哪种读文件的方式就是通过 NodeEnvironmentPlugin 插件来做的
  • 在打包完成后,需要先清空 dist 文件夹,就是通过 CleanWebpackPlugin 插件来完成的
  • ......

而实现这一切的核心就是 tapable。Webpack 内部通过 tapable 会提前定义好一系列不同阶段的 hook ,然后在固定的时间点去执行(触发 call 函数)。而插件要做的就是通过 tap 函数注册自定义事件,从而让其控制在 Webapack 事件流上运行:

image.png

继续拿 Vue 和 React 举例,就好像框架内部定义了一系列的生命周期,而我们要做的就是在需要的时候定义好这些生命周期函数就好。

9fca05f9-1d48-436a-bafd-f45d808a3b49.jpg

🏊‍♀️ Compiler 和 Compilation 

在插件开发中还有两个很重要的资源:compilercompilation对象。理解它们是扩展 Webpack 引擎的第一步。

  • compiler 对象代表了完整的 webpack 生命周期。这个对象在启动 Webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin。当在 Webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 Webpack 的主环境。
  • compilation 对象代表了一次资源版本构建。当运行 Webpack 开发环境中间件( webpack-dev-server)时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

5dd8339b-4ebd-4007-9e14-f1ca58c17d53.jpg

还是拿 React 框架举例子...... React:

590d1a42-2273-41c5-8ac2-d9e9ed90cf76.jpg

compiler比喻成 React 组件,在 React 组件中有一系列的生命周期函数(componentDidMount()render()componentDidUpdate()等等),这些钩子函数都可以在组件中被定义。

compilation比喻成 componentDidUpdate()componentDidUpdate()只是组件中的某一个钩子,它专门负责重复渲染的工作(compilation只是compiler中某一阶段的 hook ,主要负责对模块资源的处理,只不过它的工作更加细化,在它内部还有一些子生命周期函数)。

如果还是不理解,这里画个图帮助大家理解:

image.png

图上的 entryOptionafterPluginsbeforeRuncompilation 等均是构建过程中的生命周期,而 compilation 只是该过程中的其中一部分,它主要负责对模块资源的处理。在 compilation 内部也有自己的一系列生命周期,例如图中的 buildModulefinishModules 等。

cf8d3c96-74d9-434b-a27d-060dc5244311.jpg

至于为什么要这么处理,原因当然是为了解耦!!!

比如当我们启动 Webpack 的 watch模式,当文件模块发生变化时会重新进行编译,这个时候并不需要每次都重新创建 compiler 实例,只需要重新创建一个 compilation 来记录编译信息即可

另外,图中并没有将全部的 hook 展示出来,更多的hook可以自行查阅官网:compiler上挂载的 hookcompilation上挂载的 hook

ef377b09-4933-4828-9ce1-6bd2b2536e6f.jpg

🏃 如何编写插件

说了这么多,到底要怎么写一个 Webpack 插件?小舒还等着我呢!!!

bb9b6a98-7058-48fc-9ec5-bc84d7bcf8f2.jpg

刚才知道了在 Webpack 中的 compilercompilation 对象上挂载着一系列的生命周期 hook ,那接下来应该怎么在这些生命周期中注册自定义事件呢?

webpack 插件:

cb597c1b-a21a-4b8d-9fa8-b129380a3b9e.jpg

Webpack Plugin 其实就是一个普通的函数,在该函数中需要我们定制一个 apply 方法。当 Webpack 内部进行插件挂载时会执行 apply 函数。我们可以在 apply 方法中订阅各种生命周期钩子,当到达对应的时间点时就会执行。

bb740142-acbd-49da-898c-e0e765ec6552.jpg

这里可能有同学要问了,为什么非要定制一个apply方法?为什么不是其他的方法?

在这里我贴下官方源码:github.com/webpack/web… , 大家一看便一目了然:

if (options.plugins && Array.isArray(options.plugins)) {
  //这里的options.plugins就是webpack.config.js中的plugins
  for (const plugin of options.plugins) {
    plugin.apply(compiler); //执行插件的apply方法
  }
}

这里官方写死了执行插件中的 apply 方法....,并没有什么很高深的原因.....

68456079-6083-46b1-9248-97f943d7d06d.jpg

那我们就按照规范写一个简易版的插件赶紧来练练手:在构建完成后打印日志。

首先我们需要知道构建完成后对应的的生命周期是哪个,通过 查阅文档得知是 complier 中的done 这个 hook :

image.png

接下来创建一个新项目验证我们的想法,时间不早了!小舒现在肯定很着急!!!

安装依赖:

npm init //初始化项目
yarn add webpack webpack-cli -D

安装完项目依赖后,根据以下目录结构来添加对应的目录和文件:

├── dist # 打包输出目录
├── plugins # 自定义插件文件夹
│   └── demo-plugin.js
├── node_modules
├── package-lock.json
├── package.json
├── src # 源码目录
│   └── index.js # 入口文件
└── webpack.config.js # webpack配置文件

demo-plugin.js

class DemoPlugin {
  apply(compiler) {
    //在done(构建完成后执行)这个hook上注册自定义事件
    compiler.hooks.done.tap("DemoPlugin", () => {
      console.log("DemoPlugin:编译结束了");
    });
  }
}

module.exports = DemoPlugin;

package.json

{
  "name": "webpack-plugin",
  "version": "1.0.0",
  "description": "",
  "license": "ISC",
  "author": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "tapable": "^2.2.1",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0"
  }
}

src/index.js

console.log("author:""不要秃头啊");

webpack.config.js

const DemoPlugin = require("./plugins/demo-plugin");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  devtool: false,
  plugins: [new DemoPlugin()],
};

运行 yarn build,运行结果:

yarn build
$ webpack
DemoPlugin:编译结束了
asset main.js 643 bytes [emitted] (name: main)
./src/index.js 476 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 71 ms
✨  Done in 0.64s.

db4cafab-ece6-4bbb-8103-79e4589f0ebe.png

💘 开始我的表白之路....

好了,终于搞清楚怎么写插件了!!!

39d696c9-90ee-4544-9999-c056db959cfc.jpg

直接把刚才学的的demo插件改造一下:

class DonePlugin {
  apply(compiler) {
    //在done(构建完成后执行)这个hook上注册自定义事件
    compiler.hooks.done.tap("DonePlugin", () => {
      console.log(
        "小姐姐,我知道此刻你很意外。但不知道怎么回事,我看见你的第一眼就沦陷了...可以给我一个多了解了解你的机会吗? ————来自一个热心帮你解决问题的人"
      );
    });
  }
}
module.exports = DonePlugin;

正准备提交代码,思来想去,直接叫小姐姐好像不太好吧?是不是显得我很轻浮?

再说了,小舒怎么知道我在跟她说呢?

想了一会,不如直接用她的 git 账号名吧(当时要是脑子不抽风就好了......😭),于是改成动态获取git 用户名,为了显眼甚至还加了点颜色:

const chalk = require("chalk");//给日志加颜色插件
const execSync = require("child_process").execSync;

const error = chalk.bold.red; //红色日志
const warning = chalk.keyword("orange"); //橘色日志

class DonePlugin {
  apply(compiler) {
    compiler.hooks.done.tap("DonePlugin", () => {
      //获取git账号信息的username
      let name = execSync("git config user.name").toString().trim();

      console.log(
        error(`${name},`),
        warning(
          "我知道此刻你很意外。但不知道怎么回事,我看见你的第一眼就沦陷了...可以给我一个多了解了解你的机会吗?  ————来自一个热心帮你解决问题的人"
        )
      );
    });
  }
}

module.exports = DonePlugin;

大致效果就是这样...

image.png

98936199-cfea-4dce-afd1-a25b2a8b1f58.jpg

😳 等待回应

把这一切都准备妥当后,剩下的就交给天意了。

结果是左等右等,到了下午四点迟迟没有等到小舒的回应......

072c64a4-ff38-4a44-83f0-40c754980149.jpg

难道是没看到吗?不应该啊,日志还加了颜色,很明显了!!!

莫非是女孩子太含蓄了,害羞了?

不行,我得主动出击!!

image.png

乘兴而去,败兴而归!!!还在同事圈里闹了个笑话!!!

但是为了下半生,豁出去了!!!

经过我的一番解释,小舒总算相信了我说的话,而我也赶紧去优化了一下代码......

自此以后,每天一句不重样的小情话,小舒甚至还和我互动了起来:

image.png

就这样,我们慢慢的发展成了无话不谈的男女朋友关系,直到前两天甚至还过了1000天纪念日,还给小舒送了点小礼物,虽然被骂直男...

image.png

接下来也该考虑结婚了!!!

“滴~~~,滴~~~,滴~~~,不要命了!等个红绿灯都能睡着?“

“喂,醒醒,醒醒。我的尿黄,让我去渍醒他!”

只听旁边有人说到......

原来只是黄粱一梦。

38d25691-8bdb-4d7b-9b5c-adb0b090c206.jpg

💔 最后的结局

最后,给大家一个忠告:追女孩子一定不要这样, 一定要舍得送花,一定要懂浪漫!!!没有哪个女孩子会因为你写个插件就跟你在一起的!!!

我决定勇敢的试一试:

image.png

卒。

本文收录于 从零到亿系统性的建立前端构建知识体系✨ 中的第五篇。

推荐阅读

  1. 从零到亿系统性的建立前端构建知识体系✨
  2. 我是如何带领团队从零到一建立前端规范的?🎉🎉🎉
  3. 二十张图片彻底讲明白Webpack设计理念,以看懂为目的
  4. 【中级/高级前端】为什么我建议你一定要读一读 Tapable 源码?
  5. 前端工程化基石 -- AST(抽象语法树)以及AST的广泛应用
  6. 线上崩了?一招教你快速定位问题!
  7. 从构建产物洞悉模块化原理
  8. Webpack深度进阶:两张图彻底讲明白热更新原理!
  9. 【万字长文|趣味图解】彻底弄懂Webpack中的Loader机制
  10. Esbuild深度调研:吹了三年,能上生产了吗?