写完 Rspack 插件才发现,webpack 那套经验全都能直接用——官方文档里没写清楚的那几个关键点

写完了,构建产物里混进了不该有的测试代码,每次都要手动删——这个痛我相信做过二次开发的前端工程师都懂。后来我自己写了个 Rspack 插件把它自动化了,才发现 Rspack 的插件系统其实比想象中简单太多。

Rspack 兼容 webpack 插件生态,你平时在 Webpack 里怎么写插件,搬到 Rspack 里基本不用改。但它也有自己的一套 plugin API,搞清楚几个核心概念就能动手了。

先说最基础的插件结构。Rspack 插件只需要一个 apply 方法,接收 compiler 实例:

const PLUGIN_NAME = 'MyPlugin';

export class MyPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
      console.log('构建开始了!');
    });
  }
}

如果你用 TypeScript,可以直接从 @rspack/core 导入类型,IDE 补全会舒服很多:

import type { Compiler, RspackPluginInstance } from '@rspack/core';

const PLUGIN_NAME = 'MyPlugin';

export class MyPlugin implements RspackPluginInstance {
  apply(compiler: Compiler) {
    compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
      console.log('构建开始了!');
    });
  }
}

这就是全部了。一个 apply 方法,一个 compiler 对象,搞定。

compiler 对象上有五大类 hooks,这是 Rspack 插件系统的核心:Compiler hooks 管整个构建生命周期,Compilation hooks 管单次构建过程,RuntimePlugin hooks 管运行时代码生成,NormalModuleFactory hooks 管模块创建,Stats hooks 管统计信息输出。

最常用的是 Compiler hooks 和 Compilation hooks。

Compiler hooks 里,按执行顺序排列,核心这几个:

  • environment:初始化插件环境后触发,做初始化操作
  • entryOption:入口配置处理完毕后触发,可以读取 entry 配置
  • afterPlugins:内部插件注册完成后触发
  • beforeRun / run:开始构建前触发,beforeRun 只在 rspack build 命令下触发,watch 模式用 watchRun
  • compile:每次新编译前触发,在 beforeCompile 之后
  • thisCompilation / compilation:创建 compilation 对象后触发,这里可以注册 Compilation hooks
  • make:开始构建模块图前触发,是异步并行 hook,适合做耗时操作
  • finishMake:模块图构建完成后触发
  • afterCompile:seal 阶段前触发
  • shouldEmit:返回 true/false 控制是否输出产物
  • emit:输出产物到目录前触发
  • afterEmit:输出完成后触发
  • done:整个构建完成后触发
  • failed:构建失败时触发

我之前遇到的问题——构建产物里混了测试代码——就是在 emit 这个 hook 里解决的。因为这时候所有产物已经生成好了,但还没有写入磁盘,我可以在这里对 compilation.assets 做最后处理。

apply(compiler) {
  compiler.hooks.emit.tap('RemoveTestAssets', (compilation) => {
    Object.keys(compilation.assets).forEach(filename => {
      if (filename.includes('.test.') || filename.includes('__tests__')) {
        delete compilation.assets[filename];
      }
    });
  });
}

Compilation hooks 更细粒度,主要管单个构建的过程。常用的:

  • buildModule:某个模块开始构建前,可以修改模块
  • succeedModule:模块构建成功后,可以做一些统计
  • finishModules:所有模块构建完成,所有模块没有错误
  • seal:停止接受新模块,开始优化阶段
  • optimizeModules / afterOptimizeModules:模块优化前后
  • optimizeTree:优化依赖树
  • optimizeChunkModules:优化 chunk 模块
  • additionalTreeRuntimeRequirements:添加运行时依赖

有一个关键点要记住:Rspack 的核心编译逻辑跑在 Rust 侧,JS 这边的 hooks 大部分是”只读”的——你在 hook 里修改的对象变化不会同步回 Rust 侧。所以如果你想修改编译行为,需要找到正确的 hook 位置,而不是在 seal 阶段乱改。

比如我想给构建产物加一个 hash 信息,我知道该用 additionalTreeRuntimeRequirements 这个 hook,因为它可以向运行时需求集合里添加 RuntimeGlobals:

apply(compiler) {
  const { RuntimeGlobals } = compiler.rspack;

  compiler.hooks.thisCompilation.tap('HashPlugin', (compilation) => {
    compilation.hooks.additionalTreeRuntimeRequirements.tap(
      'HashPlugin',
      (chunk, set) => {
        set.add(RuntimeGlobals.getFullHash);
      }
    );
  });
}

这样运行时代码里就能用 __webpack_require__.h 获取当前构建的 hash。

还有一个很实用的场景:想让插件同时兼容 Rspack 和 webpack。Rspack 官方文档里给了一个判断方法:

apply(compiler) {
  if (compiler.rspack) {
    // 在 Rspack 里运行的逻辑
  } else {
    // 在 webpack 里运行的逻辑
  }
}

这个 compiler.rspack 属性是 Rspack 特有的,webpack 里不存在。

最后说一下怎么发布插件。如果你的插件只给 Rspack 用,在 package.json 里声明 @rspack/core 为可选依赖:

{
  "peerDependencies": {
    "@rspack/core": "^1.0.0 || ^2.0.0"
  },
  "peerDependenciesMeta": {
    "@rspack/core": {
      "optional": true
    }
  }
}

如果要同时兼容 webpack 和 Rspack,两个都声明。

写到这里,我发现 Rspack 插件开发的门槛其实很低——你只需要理解 compiler 和 compilation 两个对象,以及它们上面的 hooks 体系。剩下的就是根据你的业务场景,选对 hook 位置,填业务逻辑。

下一步你可以从自己项目的痛点出发,比如构建速度慢、产物需要二次处理、想在构建时注入环境变量……这些都可以用插件解决。官方文档的 Plugin API 部分列了完整的 hook 清单,有需要可以对着文档查。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 1725 阅读