一杯 ☕ 时间学会编写 webpack 插件

在现代前端开发中,webpack 已经成为了一个不可或缺的工具;

开发过程中,大家也或多或少接触过 webpack 中各式各样的插件。

可以说 插件系统赋予了 webpack 丰富的可扩展性 ,让我们可以根据项目需求定制化地配置和扩展 webpack 的功能。

今天我们就用一杯 ☕ 的时间,教你实现一个简单的 webpack 插件!

插件的本质

要想学会编写插件,首先得弄明白 webpack 插件到底是什么。

apply 方法

其实很简单,webpack 的插件本质上就是一个 拥有 apply 方法的函数或类;

不信?那我们扒几个插件源码瞅瞅:

  • html-webpack-plugin:

  • copy-webpack-plugin

从上面的代码中可以看出,这些插件中都实现了 apply 方法;

当 webpack 在初始化时就会调用这个 apply 方法,并传入一个参数 compiler 对象。

这里的 compiler 包含了 当前 webpack 环境的各种配置信息,以及关于模块和编译的所有信息。

其中,对于插件来说最重要的就是 上面挂载了许多的 hook ,通过 compiler.hooks.钩子名称 的语法,我们就能拿到对应的 hook 来进行一系列操作。

hook 介绍

hook 就类似于我们写 Vue 时的生命周期函数。

webpack 会在不同的构建流程中触发对应的 hook,并传入不同的上下文参数;

可以说,编写插件的过程就是 ------ 找到对应的 hook 并注册,然后在回调函数中使用传入的上下文参数实现逻辑的过程,比如:

js 复制代码
// 通过 compiler.hooks.compilation 拿到对应钩子
// 通过 tap 方法来注册回调
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
  // 在回调函数中拿到参数 compilation
  // 执行一些逻辑
});

webpack 提供了几百种 hook ,我们来简单了解几个 hook 的使用:

compiler.hooks.compilation

触发时机

webpack 在每次编译过程中都会新建一个 compilation 对象,这个钩子就会在 compilation 对象新建时被触发;

上下文参数:

这个钩子的上下文参数是当前编译期间的 compilation 对象;同时,compilation 对象上又挂载了其它的许多钩子可供调用:

js 复制代码
// 在每次新建 Compilation 对象时触发
compiler.hooks.compilation.tap('FirstPlugin', (compilation) => {
  // 这个钩子在 webpack 处理 Chunk ID 的阶段触发
  compilation.hooks.chunkIds.tap('SecondPlugin', () => {})
});

compiler.hooks.emit

触发时机

当 webpack 即将生成文件(将要输出资源到输出目录)时,compiler.hooks.emit 钩子会被触发。

上下文参数:

它的参数同样是当前编译期间的 compilation 对象。

compiler.hooks.done

触发时机

当 webpack 完成编译、打包并且输出结果后触发。

上下文参数:

这个钩子的上下文参数是 stats,也就是本次 编译的相关信息;

通过监听这个钩子,我们就可以做输出日志、通知其他系统、执行清理等操作。

webpack 完整 hook 目录戳这里 👉 webpack hook

hook 类型

了解了 webpack 提供了哪些 hook,你就已经掌握了一大半插件的工作原理啦。

现在不妨来思考一个问题:如果我们在一个 hook 上注册了多个回调函数,那么这些回调函数会被如何调用呢?

比如说,下面这段代码它的输出顺序是什么:

js 复制代码
compiler.hooks.compile.tap('FirstPlugin', 
  () => console.log('FirstPlugin')
);
compiler.hooks.compile.tap('SecondPlugin', 
  () => console.log('SecondPlugin')
);

实际上,上面这段代码的输出顺序取决于 hook 的类型。

对于 compile 这个 hook 来说,它是一个 同步的钩子(SyncHook) ,因此它的输出顺序就是 先输出 'FirstPlugin' ,然后输出 'SecondPlugin';

对于不同的类型的 hook,还提供不同的注册方式来注册回调函数。

下面我们一起来看看 webpack 中具体有哪些类型的 hook:

同步钩子(SyncHook)

这些钩子用于同步操作,它们的回调函数会 按照注册的顺序依次执行。

  • 注册方式:只能使用 tap 方法;
  • 相应 hook :compile、thisCompilation``compilation 等等;
js 复制代码
compiler.hooks.compile.tap('MyPlugin', (params) => {
  console.log('我是一个同步钩子');
});

同步熔断钩子(SyncBailHook)

这些钩子也是同步的。但如果 回调函数返回非 undefined 的值,钩子的执行将停止,后续的回调函数将不会被调用。

  • 注册方式:只能使用 tap 方法;
  • 相应 hook :entryOption、shouldEmit、log 等等;
js 复制代码
compiler.hooks.shouldEmit.tap('MyPlugin', (compilation) => {
  //返回 false 会阻断后续回调的调用
  return false; 
});

同步瀑布流钩子(SyncWaterfallHook)

这些钩子是 同步的。它们的每个监听函数都会按照注册的顺序被调用,并 将前一个回调函数的返回值传递给下一个回调函数。

  • 注册方式:只能使用 tap 方法;
  • 相应 hook :assetPath、contextModuleFiles;
js 复制代码
compiler.hooks.thisCompilation.tap('MyPlugin', (compilation) => {
  compilation.hooks.assetPath.tap(
    'MyPlugin',
    (filename, data) => {
      // 将自定义的文件名传给下一个回调函数
      return 'custom_prefix_' + filename;
    }
  );
});

异步并行钩子(AsyncParallelHook)

这些钩子支持异步操作,回调函数可以 并行执行。

回调函数必须 调用入参中的 callback 或返回 Promise。

  • 注册方式:可以使用 tap、tapAsync、tapPromise 方法。
    • 对于使用 tapAsync 注册的回调函数,Webpack 会 等待所有回调函数调用 callback 方法之后才会继续执行。
    • 对于使用 tapPromise 注册的回调函数,Webpack 会 等待所有返回的 Promise 都 resolve 之后才会继续执行。
  • 相应 hook :make。
js 复制代码
compiler.hooks.make.tapAsync('MyPlugin', (compilation, callback) => {
  callback();
});

// 或者

compiler.hooks.afterEmit.tapPromise('MyPlugin', (compilation) => {
  return new Promise((resolve) => {
    resolve();
  });
});

异步串行钩子(AsyncSeriesHook)

这些钩子也支持异步操作,但回调函数会 按照注册的顺序一个接一个依次执行。

  • 注册方式:可以使用 tap、tapAsync、tapPromise 方法。调用逻辑同异步并行钩子(AsyncParallelHook)。
  • 相应 hook :run、emit、afterEmit、assetEmitted 等等。

异步瀑布流钩子(AsyncSeriesWaterfallHook)

这些钩子支持异步操作,它们的每个回调函数都会 按照注册的顺序被调用,并将前一个回调函数解析的值传递给下一个监听函数。

  • 注册方式:可以使用 tap、tapAsync、tapPromise 方法。调用逻辑同异步并行钩子(AsyncParallelHook)。
  • 相应 hook :beforeResolve、afterResolve、alternativeRequests 等等。

小试牛刀

到这里,你已经完全掌握了 webpack 插件的奥秘!

小伙伴们不妨动动手指来试着编写一个插件,用来在 webpack 编译完成后输出相应的编译信息。

下面是具体的插件代码:

js 复制代码
// CompileInfoPlugin.js

const fs = require('fs')
const path = require('path')
class BuildInfo {
  apply (compiler) {
    // 通过 done 这个钩子注册一个在编译完成后执行的钩子
    // 这里回调函数中的入参 stats 就是本次编译相关信息
    compiler.hooks.done.tap('CompileInfoPlugin', (stats) => {
      // 编译完成后输出一条信息
      console.log('Compile is done.')
      // 将编译信息输出到 dist 目录下的 report 文件中
      const filepath = path.join(process.cwd(), './dist/report')
      const str = stats.toString({
        colors: true,
        modules: false,
        children: false,
        chunks: false,
        chunkModules: false
      })
      fs.writeFileSync(filepath, str)
    })
  }
}

// 导出模块
module.exports = {
  BuildInfo
}

接下来在 webpack 配置中使用上面的插件:

js 复制代码
// webpack.config.js

const CompileInfoPlugin = require('./CompileInfoPlugin');

module.exports = {
  // ... 其他配置项
  plugins: [
    new CompileInfoPlugin()
  ]
};

大功告成!🎉🎉🎉

相关推荐
考研保研资料分享4 小时前
2022年马理论保研复盘:南师大原著题、首师大追问与上财选择
面试
考研保研资料分享4 小时前
2022编辑出版跨保西北大学中国史:五校面试与九月补材料
面试·职场和发展
Data analyse4565 小时前
rnweb 回放数据量太大怎么办?分片存储与断点续传的实现思路
前端·网络·算法·数据分析
xiaogai_gai5 小时前
金蝶收料通知单查询策略实战教程:从源系统拉取到中间层落地的完整路径
java·前端·数据库
环境栈笔记6 小时前
指纹浏览器 WebRTC 怎么设置?从 ICE 候选、代理出口到功能验证
前端·人工智能·后端·自动化
赵大仁7 小时前
官网性能优化实战:LCP 3.2s → 2.1s、CLS 0.107 → 0,以及被我推翻的 6 个判断
前端·性能优化·cls·lcp·core web vitals
xiancai_xianyu7 小时前
Text2SQL落地生产库前,SQL要过哪三道闸
前端·数据库·sql·sql注入·text2sql·ai agent开发·企业级ai框架
小羊没烦恼!7 小时前
T-SQL查询进阶--理解SQL SERVER中的分区表
服务器·开发语言·前端·javascript·html
史瓦辛格7 小时前
给 DeepSeek Harness 写费用插件:把「这次会话花了多少钱」算准
前端·deepseek
星光不问赶路人7 小时前
Vue 3 + TypeScript 中 enum 的使用实践
前端·vue.js