close
  • 简体中文
  • 插件开发

    插件系统是 Rsbuild 架构的核心,Rsbuild 的大部分功能都是通过插件实现的,这种设计让核心保持轻量,同时提供了灵活的扩展性。

    Rsbuild 插件是一个函数,它可以在不同阶段注册钩子,监听事件并执行自定义逻辑。无论你想要修改默认行为、添加新功能,还是集成第三方工具,插件都提供了丰富的 API 来实现这些需求。

    对比其他插件

    在开发 Rsbuild 插件之前,你可能已经接触过 webpack、Vite、esbuild 等工具的插件系统。

    总体而言,Rsbuild 的插件 API 和 esbuild 相似,与 webpack 或 Rspack 插件相比,Rsbuild 的插件 API 更加简洁和容易上手。

    // esbuild plugin
    const esbuildPlugin = {
      name: 'example',
      setup(build) {
        build.onEnd(() => console.log('done'));
      },
    };
    
    // Rsbuild plugin
    const rsbuildPlugin = () => ({
      name: 'example',
      setup(api) {
        api.onAfterBuild(() => console.log('done'));
      },
    });
    
    // Rspack plugin
    class RspackExamplePlugin {
      apply(compiler) {
        compiler.hooks.done.tap('RspackExamplePlugin', () => {
          console.log('done');
        });
      }
    }

    从功能上看,Rsbuild 的插件 API 主要围绕 Rsbuild 的运行流程和构建配置,并提供一些 hooks 用于扩展。而 Rspack 的插件 API 则更加复杂和丰富,能够修改打包过程的每一个环节。

    Rsbuild 插件中可以集成 Rspack 插件,如果 Rsbuild 提供的 hooks 无法满足你的需求,你也可以通过 Rspack 插件来实现功能,并在 Rsbuild 插件中注册 Rspack 插件:

    const rsbuildPlugin = () => ({
      name: 'example',
      setup(api) {
        api.modifyRspackConfig((config) => {
          config.plugins.push(new RspackExamplePlugin());
        });
      },
    });

    开发插件

    插件提供类似 (options?: PluginOptions) => RsbuildPlugin 的函数作为入口。

    插件示例

    pluginFoo.ts
    import type { RsbuildPlugin } from '@rsbuild/core';
    
    export type PluginFooOptions = {
      message?: string;
    };
    
    export const pluginFoo = (options: PluginFooOptions = {}): RsbuildPlugin => ({
      name: 'plugin-foo',
    
      setup(api) {
        api.onAfterStartDevServer(() => {
          const msg = options.message || 'hello!';
          console.log(msg);
        });
      },
    });

    注册插件:

    rsbuild.config.ts
    import { pluginFoo } from './pluginFoo';
    
    export default {
      plugins: [pluginFoo({ message: 'world!' })],
    };

    插件结构

    函数形式的插件可以 接受选项对象返回插件实例,并通过闭包机制管理内部状态。

    其中各部分的作用分别为:

    • name 属性用于标注插件名称。
    • setup 作为插件逻辑的主入口。
    • api 对象包含了各类钩子和工具函数。

    命名规范

    插件的命名规范如下:

    • 插件的函数命名为 pluginAbc,并通过具名导出。
    • 插件的 name 采用 scope:foo-barplugin-foo-bar 格式,添加 scope: 可以避免和其他插件产生命名冲突。

    下面是一个例子:

    pluginFooBar.ts
    import type { RsbuildPlugin } from '@rsbuild/core';
    
    export const pluginFooBar = (): RsbuildPlugin => ({
      name: 'scope:foo-bar',
      setup() {},
    });
    Tip

    Rsbuild 官方插件的 name 统一使用 rsbuild: 作为前缀,比如 rsbuild:react 对应 @rsbuild/plugin-react

    模板仓库

    rsbuild-plugin-template 是一个最小的 Rsbuild 插件模板仓库,你可以基于该仓库来开发你的 Rsbuild 插件。

    Environment 插件

    Rsbuild 支持同时为多个环境构建产物,并支持某个插件仅在指定环境下运行

    当你希望你开发的插件支持作为 Environment 插件使用时,需要注意以下几点:

    1. 每个 environment 有自身的 Rsbuild 配置:
    2. 避免副作用,你的插件代码可能执行多次:
      • 当同一个插件在不同环境下注册多次时,会被视为多个 Rsbuild 插件(哪怕它们指向同一个插件实例),这是因为它们带有不同的 Rsbuild environment 上下文。

    下面是一个 Environment 插件例子:

    pluginFoo.ts
    import type { RsbuildPlugin } from '@rsbuild/core';
    
    export type PluginFooOptions = {
      title?: string;
    };
    
    export const pluginFoo = (options: PluginFooOptions = {}): RsbuildPlugin => ({
      name: 'plugin-foo',
    
      setup(api) {
        api.modifyEnvironmentConfig((config) => {
          config.html.title = options.title || 'My Default Title';
        });
        api.modifyBundlerChain((chain, { environment }) => {
          chain.name(environment.config.html.title);
        });
      },
    });

    引用其他插件

    Rsbuild 的 plugins 配置项支持传入一个嵌套的数组,这意味着你可以通过这种方式在插件内部引用其他 Rsbuild 插件。

    例如,在 pluginFoo 内部引用并注册 pluginBar

    import { pluginBar } from 'rsbuild-plugin-bar';
    
    export const pluginFoo = (): RsbuildPlugin => {
      const foo = {
        name: 'plugin-foo',
        setup(api) {
          // ...
        },
      };
      return [foo, pluginBar()];
    };

    生命周期钩子

    Rsbuild 在内部按照约定的生命周期进行任务调度,插件可以通过注册钩子来介入工作流程的任意阶段,并实现自己的功能。

    Rsbuild 生命周期钩子的完整列表参考 API 文档

    Rsbuild 不会接管底层 Rspack 的生命周期,相关生命周期钩子的使用方式见对应文档:Rspack Plugin API

    迁移 Vite 插件

    参考 迁移 Vite 插件 了解如何迁移一个 Vite 插件到 Rsbuild 插件。

    读写 Rsbuild 配置

    当插件需要读取或修改项目的 Rsbuild 配置时,可以使用 Rsbuild 提供的配置 API。

    修改基础配置

    setup 中注册 api.modifyRsbuildConfig,可以在基础配置与各个 environment 的配置合并前对其进行修改:

    api.modifyRsbuildConfig((config) => {
      config.output.minify = false;
    });

    modifyRsbuildConfig 是全局 hook。如果修改仅针对部分 environment,或需要根据当前 environment 调整配置,建议改用 api.modifyEnvironmentConfig。详细说明请参考全局 hooks 与 environment hooks

    读取规范化后的配置

    配置修改 hooks 执行完毕后,可以无参数调用 api.getNormalizedConfig,获取包含所有 environment 的完整配置。该配置已经过规范化处理并包含默认值,类型也比 api.getRsbuildConfig 的返回值更明确。

    api.onBeforeBuild(() => {
      const config = api.getNormalizedConfig();
      console.log(Object.keys(config.environments));
    });

    如果当前没有 environment context,但需要读取某个 environment 的配置,可以将其名称传给 getNormalizedConfig

    api.onBeforeBuild(() => {
      const config = api.getNormalizedConfig({ environment: 'web' });
      console.log(config.output.target);
    });

    返回值类型请参考 NormalizedConfigNormalizedEnvironmentConfig

    读取当前环境的配置

    当 hook 的回调参数中包含 environment context 时,建议通过 environment.config 获取配置。该配置由基础配置与当前 environment 的配置合并并经过规范化处理后得到。

    api.onBeforeEnvironmentCompile(({ environment }) => {
      const { name, config } = environment;
      console.log(`${name}: ${config.output.target}`);
    });

    读取所有环境的配置

    onBeforeBuildonAfterBuild 等全局 hooks 会提供 environments,其中包含所有 environment 的上下文。当插件需要读取每个 environment 的配置时,可以遍历该对象:

    api.onBeforeBuild(({ environments }) => {
      for (const { name, config } of Object.values(environments)) {
        console.log(`${name}: ${config.output.distPath.root}`);
      }
    });

    多环境配置的详细说明请参考多环境构建

    修改 Rspack 配置

    Rsbuild 插件允许你修改内置的 Rspack 配置,包括:

    示例

    比如,通过 Rsbuild 插件来注册 eslint-rspack-plugin

    import type { RsbuildPlugin } from '@rsbuild/core';
    import ESLintRspackPlugin from 'eslint-rspack-plugin';
    
    export const pluginEslint = (options?: Options): RsbuildPlugin => ({
      name: 'plugin-eslint',
      setup(api) {
        api.modifyRspackConfig((config) => {
          config.plugins.push(
            new ESLintRspackPlugin({
              // plugins options
            }),
          );
        });
      },
    });

    扩展插件 API

    当你基于 Rsbuild 的 JavaScript API 来实现自定义的工具时,可能希望在现有插件 API 的基础上,提供更多能力,例如添加工具方法或共享上下文对象。

    此时,你可以使用 Rsbuild 实例上的 rsbuild.expose() 方法。它的作用与插件的 api.expose() 一致,用于向 Rsbuild 插件暴露自定义的方法或对象。

    例如,向插件暴露 getStatesetCount 方法:

    myToolkit.ts
    import { createRsbuild } from '@rsbuild/core';
    
    export const MY_TOOLKIT_ID = 'my-toolkit';
    
    const rsbuild = await createRsbuild({
      // ...
    });
    
    const state = {
      count: 0,
    };
    
    rsbuild.expose(MY_TOOLKIT_ID, {
      getState() {
        return state;
      },
      setCount(count: number) {
        state.count = count;
      },
    });

    然后,插件可以通过 api.useExposed() 方法访问这些扩展 API:

    myPlugin.ts
    import { MY_TOOLKIT_ID } from './myToolkit';
    
    const myPlugin = {
      name: 'my-plugin',
      setup(api) {
        const toolkitApi = api.useExposed(MY_TOOLKIT_ID);
        if (toolkitApi) {
          const { count } = toolkitApi.getState();
          toolkitApi.setCount(count + 1);
        }
      },
    };

    依赖声明

    发布 Rsbuild 插件时,应该在 package.json 中声明 @rsbuild/corepeerDependencies,并在 devDependencies 中安装它用于开发:

    {
      "peerDependencies": {
        "@rsbuild/core": "^2.0.0"
      },
      "devDependencies": {
        "@rsbuild/core": "^2.0.0"
      }
    }

    如果插件只引用了 @rsbuild/core 的类型导出,可以将其声明为 optional peer dependency:

    {
      "peerDependencies": {
        "@rsbuild/core": "^2.0.0"
      },
      "peerDependenciesMeta": {
        "@rsbuild/core": {
          "optional": true
        }
      }
    }

    这种情况下,插件在被基于 Rsbuild 的上层工具(如 Rslib 或 Rspress)使用时,不会产生不必要的 peer dependency 警告。