前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

webpack plugin原理分析与实践 手写四个实用插件

首页2021-01-05 12:30:23Front-End
Webpack插件前端工程化

配置文件里的 plugins 数组我抄了好几年,塞进去七八个插件都能跑,可一旦某个插件行为不对,我完全不知道该从哪儿开始查。真正逼我把这块弄明白的是个很小的需求,构建完想自动生成一份「这次打出来哪些文件、各多大」的清单。现成的方案不是没有,只是我想顺手把 plugin 的写法练一遍。

几十行代码写完,事件流、Compiler 和 Compilation 的分工、emit 到底卡在哪个时机,一下子全串起来了。这篇把原理和四个能直接抄走的插件例子记在一起,末尾单独补一段 webpack 4 和 5 之后哪些写法已经不能用了。读完你至少能自己判断一个功能该挂在哪个钩子上。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 一个插件挂到 webpack 上的完整过程,apply 和 compiler 是怎么传进来的
  • Compiler 和 Compilation 的区别,什么时候该用哪个
  • Tapable 的九种钩子,同步、异步串行、异步并行分别怎么注册和触发
  • 常用 API,读产物、监听额外文件变化、改写输出、判断当前用了哪些插件
  • 四个可以直接抄走的插件实现,构建收尾、文件清单、版权信息、打包 zip
  • webpack 4 到 5 之后,文中这些写法哪些废弃了,替代品是什么

# 一、一个插件是怎么挂上去的

webpack 跑起来之后,整个生命周期会广播出一大堆事件。plugin 干的事就一件,监听它关心的那个事件,在那个时机用 webpack 给的 API 改变输出结果。

先看最小的一个插件长什么样。

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }
  
  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.plugin('compilation',function(compilation,callback) {

    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin;
@前端进阶之旅: 代码已经复制到剪贴板

这段代码里其实只有一个约定,插件就是一个带 apply 方法的类。构造函数拿用户传进来的配置,apply 拿 webpack 给的 compiler,别的都是你自己的事。

在配置里用它的时候是这样:

const BasicPlugin = require('./BasicPlugin.js');
module.exports = {
  plugins:[
    new BasicPlugin(options),
  ]
}
@前端进阶之旅: 代码已经复制到剪贴板

顺着这两段代码把流程串一遍。webpack 启动后读配置,先执行 new BasicPlugin(options) 拿到实例,这一步只是把你的配置存下来,还没有任何 webpack 的东西可用。等 compiler 对象初始化完,webpack 才回过头调用 basicPlugin.apply(compiler),把 compiler 交到插件手里。从这一刻起插件才算真正接入了构建流程,可以通过 compiler.plugin(事件名称, 回调函数) 监听广播出来的事件,也可以直接操作 compiler 上的配置。

所以插件的构造函数里不要写任何依赖 webpack 状态的逻辑,那时候什么都还没有。

原文这里有个笔误我顺手改了,配置导出应该是 module.exports 而不是 module.export,后者 webpack 读不到,表现是插件像不存在一样,一点报错都没有。这种错最难查,因为你会一直怀疑是钩子挂错了。

到这里最简的模型就跑通了,但实际开发绕不开两个对象,下面单独说。

# 二、Compiler 和 Compilation 到底该用哪个

开发 plugin 时最常打交道的就是这两个对象,它们是插件和 webpack 之间的桥梁。

Compiler 对象包含了 webpack 环境所有的配置信息,options、loaders、plugins 这些都挂在上面。它在 webpack 启动时被实例化,全局唯一,可以简单地把它理解为 webpack 实例本身。

Compilation 对象包含的是当前这一次编译的模块资源、编译生成资源、变化的文件等等。webpack 以开发模式运行时,每检测到一个文件变化就会创建一次新的 Compilation。它同样提供了很多事件回调供插件扩展,并且通过它也能反过来读到 Compiler。

两者的区别就一句话,Compiler 代表整个 webpack 从启动到关闭的生命周期,Compilation 只代表一次编译。

判断该用哪个有个很好用的经验:跟「这一次打包出来什么」有关的,找 Compilation;跟「整个进程什么时候开始、什么时候结束」有关的,找 Compiler。watch 模式下你会更容易理解这层关系,进程只启动一次,Compiler 就一个,但你改一次文件就多一个 Compilation。如果你把状态缓存在了 Compiler 上,watch 时它会一直累积,这个坑我踩过,插件在单次构建时正常,开着 dev server 改几次文件之后输出就重复了。

那这些钩子是哪来的?webpack 源码里 compiler 的钩子函数是借助 tapable 库实现的。

const {
    Tapable,
    SyncHook,
    SyncBailHook,
    AsyncParallelHook,
    AsyncSeriesHook
} = require("tapable");

class Compiler extends Tapable {
  constructor(context) {
      super();
      this.hooks = {
          /** @type {SyncBailHook<Compilation>} */
          shouldEmit: new SyncBailHook(["compilation"]),
          /** @type {AsyncSeriesHook<Stats>} */
          done: new AsyncSeriesHook(["stats"]),
          /** @type {AsyncSeriesHook<>} */
          additionalPass: new AsyncSeriesHook([]),
          /** @type {AsyncSeriesHook<Compiler>} */
          beforeRun: new AsyncSeriesHook(["compiler"]),
          /** @type {AsyncSeriesHook<Compiler>} */
          run: new AsyncSeriesHook(["compiler"]),
          /** @type {AsyncSeriesHook<Compilation>} */
          emit: new AsyncSeriesHook(["compilation"]),
          /** @type {AsyncSeriesHook<string, Buffer>} */
          assetEmitted: new AsyncSeriesHook(["file", "content"]),
          /** @type {AsyncSeriesHook<Compilation>} */
          afterEmit: new AsyncSeriesHook(["compilation"]),

          /** @type {SyncHook<Compilation, CompilationParams>} */
          thisCompilation: new SyncHook(["compilation", "params"]),
          /** @type {SyncHook<Compilation, CompilationParams>} */
          compilation: new SyncHook(["compilation", "params"]),
          /** @type {SyncHook<NormalModuleFactory>} */
          normalModuleFactory: new SyncHook(["normalModuleFactory"]),
          /** @type {SyncHook<ContextModuleFactory>}  */
          contextModuleFactory: new SyncHook(["contextModulefactory"]),

          /** @type {AsyncSeriesHook<CompilationParams>} */
          beforeCompile: new AsyncSeriesHook(["params"]),
          /** @type {SyncHook<CompilationParams>} */
          compile: new SyncHook(["params"]),
          /** @type {AsyncParallelHook<Compilation>} */
          make: new AsyncParallelHook(["compilation"]),
          /** @type {AsyncSeriesHook<Compilation>} */
          afterCompile: new AsyncSeriesHook(["compilation"]),

          /** @type {AsyncSeriesHook<Compiler>} */
          watchRun: new AsyncSeriesHook(["compiler"]),
          /** @type {SyncHook<Error>} */
          failed: new SyncHook(["error"]),
          /** @type {SyncHook<string, string>} */
          invalid: new SyncHook(["filename", "changeTime"]),
          /** @type {SyncHook} */
          watchClose: new SyncHook([]),

          /** @type {SyncBailHook<string, string, any[]>} */
          infrastructureLog: new SyncBailHook(["origin", "type", "args"]),

          // TODO the following hooks are weirdly located here
          // TODO move them for webpack 5
          /** @type {SyncHook} */
          environment: new SyncHook([]),
          /** @type {SyncHook} */
          afterEnvironment: new SyncHook([]),
          /** @type {SyncHook<Compiler>} */
          afterPlugins: new SyncHook(["compiler"]),
          /** @type {SyncHook<Compiler>} */
          afterResolvers: new SyncHook(["compiler"]),
          /** @type {SyncBailHook<string, Entry>} */
          entryOption: new SyncBailHook(["context", "entry"])
      };
  }}
@前端进阶之旅: 代码已经复制到剪贴板

这一大段构造函数看着吓人,其实信息量很集中。this.hooks 就是这个 Compiler 能广播的全部事件,每个事件都被声明成了某一种钩子类型,注释里的 SyncHook<Compilation> 这类泛型标注说明了回调能拿到什么参数。你要写插件,第一件事就是来这里翻,看看有没有一个钩子的时机正好是你要的。

fe
  • 一、一个插件是怎么挂上去的
  • 二、Compiler 和 Compilation 到底该用哪个
    • 2.1 事件流机制
  • 三、写插件绕不开的几个 API
    • 3.1 读取输出资源、代码块、模块及其依赖
    • 3.2 监听文件变化
    • 3.3 修改输出资源
    • 3.4 判断当前用了哪些插件
  • 四、四个能直接抄走的插件
    • 4.1 构建结束后做点别的事
    • 4.2 生成一份文件清单
    • 4.3 给产物加一份版权信息
    • 4.4 把产物打成 zip
  • 五、这几年 webpack 变了什么
  • 总结
  • 参考

← JS内存泄漏与垃圾回收机制完全梳理初探vscode插件开发 从环境搭建到发布上架 →