插件 API

上一节我们介绍了插件的基本结构,本节我们来介绍插件的 API,帮助你更细致地了解插件功能。

提示

为了获得更好的类型提示,你可以在项目中安装 @rspress/shared,然后通过 import { RspressPlugin } from '@rspress/shared' 来引入 RspressPlugin 类型。

globalStyles

  • 类型string

用于添加全局样式,传入一个样式文件的绝对路径,使用方式如下:

plugin.ts
import { RspressPlugin } from '@rspress/shared';
import path from 'path';

export function pluginForDoc(): RspressPlugin {
  // 样式路径
  const stylePath = path.join(__dirname, 'some-style.css');
  return {
    // 插件名称
    name: 'plugin-name',
    // 全局样式的路径
    globalStyles: path.join(__dirname, 'global.css'),
  };
}

比如你想修改主题色,可以通过添加全局样式来实现:

global.css
:root {
  --rp-c-brand: #ffa500;
  --rp-c-brand-dark: #ffa500;
  --rp-c-brand-darker: #c26c1d;
  --rp-c-brand-light: #f2a65a;
  --rp-c-brand-lighter: #f2a65a;
}

globalUIComponents

  • 类型(string | [string, object])[]

用于添加全局组件,传入一个数组,数组中的每一项都是一个组件的绝对路径,使用方式如下:

plugin.ts
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(): RspressPlugin {
  // 组件路径
  const componentPath = path.join(__dirname, 'foo.tsx');
  return {
    // 插件名称
    name: 'plugin-comp',
    // 全局组件的路径
    globalUIComponents: [componentPath],
  };
}

globalUIComponents 的每一项可以是一个字符串,代表组件的文件路径;也可以是一个数组,第一项为组件的文件路径,第二项为组件的 props 对象,比如:

rspress.config.ts
import { defineConfig } from 'rspress/config';
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(): RspressPlugin {
  // component path
  const componentPath = path.join(__dirname, 'foo.tsx');
  return {
    // plugin name
    name: 'plugin-comp',
    globalUIComponents: [
      [
        path.join(__dirname, 'components', 'MyComponent.tsx'),
        {
          foo: 'bar',
        },
      ],
    ],
  };
}

当你注册了全局组件之后,框架会自动将这些 React 组件在主题中进行渲染,而不用你手动引入。

通过全局组件,你可以完成诸多自定义的功能,比如:

compUi.tsx
import React from 'react';

// 需要默认导出一个组件
// 通过 props 来拿到配置中传入的 props 数据
export default function PluginUI(props?: { foo: string }) {
  return <div>This is a global layout component</div>;
}

这样,在主题页面中会渲染组件的内容,比如添加回到顶部按钮

同时,你也可以通过全局组件来注册全局副作用。比如:

compSideEffect.tsx
import { useEffect } from 'react';
import { useLocation } from 'rspress/runtime';

// 需要默认导出一个组件
export default function PluginSideEffect() {
  const { pathname } = useLocation();
  useEffect(() => {
    // 组件初次渲染时执行
  }, []);

  useEffect(() => {
    // 路由变化时执行
  }, [pathname]);
  return null;
}

这样,在主题页面中会执行组件的副作用。比如以下的一些需要副作用的场景:

  • 针对某些页面路由进行重定向操作。

  • 对页面的 img 标签进行事件监听,实现图片放大功能。

  • 路由变化时,上报不同页面的 PV 数据。

  • ......

builderConfig

  • 类型RsbuildConfig

Rspress 底层基于 Rsbuild 来进行文档构建。通过 builderConfig 可以对 Rsbuild 进行配置,具体的配置项可以参考 Rsbuild - 配置

当然,如果你想要直接配置 Rspack,也可以通过 builderConfig.tools.rspack 进行配置。

plugin.ts
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(slug: string): RspressPlugin {
  return {
    // 插件名称
    name: 'plugin-name',
    // 构建阶段的全局变量定义
    builderConfig: {
      source: {
        define: {
          SLUG: JSON.stringify(slug),
        },
      },
      tools: {
        rspack(options) {
          // 修改 rspack 的配置
        },
      },
    },
  };
}

config

  • 类型(config: DocConfig, utils: ConfigUtils, isProd: boolean) => DocConfig | Promise<DocConfig>

其中,ConfigUtils 的类型定义如下:

interface ConfigUtils {
  addPlugin: (plugin: RspressPlugin) => void;
  removePlugin: (pluginName: string) => void;
}

用于修改 Rspress 本身的配置,比如你想要修改文档的标题,可以通过 config 来实现:

plugin.ts
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(): RspressPlugin {
  return {
    // 插件名称
    name: 'plugin-name',
    // 扩展 Rspress 本身的配置
    config(config) {
      return {
        ...config,
        title: '新的文档标题',
      };
    },
  };
}

如果涉及到添加和删除插件,需要通过 addPluginremovePlugin 来实现:

plugin.ts
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(): RspressPlugin {
  return {
    // 插件名字
    name: 'plugin-name',
    // 扩展 Rspress 本身的配置
    config(config, utils) {
      // 添加插件
      utils.addPlugin({
        name: 'plugin-name',
        // ...插件的其他配置
      });
      // 通过插件名称来删除插件
      utils.removePlugin('plugin-name');
      return config;
    },
  };
}

beforeBuild/afterBuild

  • 类型(config: DocConfig, isProd: boolean) => void | Promise<void>

用于在文档构建之前/之后执行一些操作,第一个参数是文档的配置,第二个参数是当前是否是生产环境。使用方式如下:

plugin.ts
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(): RspressPlugin {
  return {
    // 插件名称
    name: 'plugin-name',
    // 在构建之前执行的钩子
    async beforeBuild(config, isProd) {
      // 这里可以执行一些操作
    },
    // 在构建之后执行的钩子
    async afterBuild(config, isProd) {
      // 这里可以执行一些操作
    },
  };
}
提醒

beforeBuild 钩子执行时,已经经过了所有插件的 config 插件处理,因此 config 参数已经代表了最终的文档配置。

markdown

  • 类型{ remarkPlugins?: Plugin[]; rehypePlugins?: Plugin[]; globalComponents?: string[] }

用于扩展 Markdown/MDX 的编译能力,如果你想要添加自定义的 remark/rehype 插件以及 MDX 里的全局组件,可以通过 markdown 配置来实现:

plugin.ts
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(): RspressPlugin {
  return {
    // 插件名称
    name: 'plugin-name',
    // 扩展 Markdown/MDX 编译能力
    markdown: {
      // 启用 JS 版本的编译器以支持外部插件
      mdxRs: false,
      remarkPlugins: [
        // 添加自定义的 remark 插件
      ],
      rehypePlugins: [
        // 添加自定义的 rehype 插件
      ],
      globalComponents: [
        // 为 MDX 注册全局组件
      ],
    },
  };
}

extendPageData

  • 类型(pageData: PageData) => void | Promise<void>

用于扩展页面数据,比如你想要在页面数据中添加一些自定义的属性,可以通过 extendPageData 来实现:

plugin.ts
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(): RspressPlugin {
  return {
    // 插件名称
    name: 'plugin-name',
    // 扩展页面数据
    extendPageData(pageData, isProd) {
      // 你可以往 pageData 对象上添加或者修改属性
      pageData.a = 1;
    },
  };
}

在扩展页面数据之后,你可以在主题中通过 usePageData 这个 hook 来访问页面数据。

import { usePageData } from 'rspress';

export function MyComponent() {
  const { page } = usePageData();
  // page.a === 1
  return <div>{page.a}</div>;
}

addPages

  • 类型(config: UserConfig) => AdditionalPage[] | Promise<AdditionalPage[]>

其中,configrspress.config.ts 配置文件中导出的 doc 属性值,AdditionalPage 的类型定义如下:

interface AdditionalPage {
  routePath: string;
  filepath?: string;
  content?: string;
}

主要用来添加额外的页面,你可以在 addPages 函数中返回一个数组,数组中的每一项都是一个页面的配置,你可以通过 routePath 来指定页面的路由,通过 filepath 或者 content 来指定页面的内容。比如:

import path from 'path';
import { RspressPlugin } from '@rspress/shared';

export function docPluginDemo(): RspressPlugin {
  return {
    name: 'add-pages',
    addPages(config, isProd) {
      return [
        //  支持真实文件的绝对路径(filepath),这样会读取磁盘中的 md(x) 内容
        {
          routePath: '/filepath-route',
          filepath: path.join(__dirname, 'blog', 'index.md'),
        },
        //  支持通过 content 参数直接传入 md(x) 内容
        {
          routePath: '/content-route',
          content: '# Demo2',
        },
      ];
    },
  };
}

addPages 接受两个参数,config 为当前文档站的配置,isProd 表示是否为生产环境。

routeGenerated

  • 类型(routeMeta: RouteMeta[]) => void | Promise<void>

这这个钩子中,你可以拿到所有的路由信息,每一项路由信息的结构如下:

export interface RouteMeta {
  // 路由
  routePath: string;
  // 文件绝对路径
  absolutePath: string;
  // 页面名称,作为打包产物文件名的一部分
  pageName: string;
  // 语言
  lang: string;
}

例子:

plugin.ts
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(): RspressPlugin {
  return {
    // 插件名称
    name: 'plugin-routes',
    // 在构建之后执行的钩子
    async routeGenerated(routes, isProd) {
      // 这里可以拿到 routes 数组,执行一些操作
    },
  };
}

addRuntimeModules

  • 类型(config: UserConfig, isProd: boolean) => Record<string, string> | Promise<Record<string, string>>;

用于添加额外的运行时模块,比如你想要在文档中使用到某些编译时的信息,可以通过 addRuntimeModules 来实现:

plugin.ts
import { RspressPlugin } from '@rspress/shared';

export function pluginForDoc(): RspressPlugin {
  return {
    // 插件名称
    name: 'plugin-name',
    // 添加额外的运行时模块
    async addRuntimeModules(config, isProd) {
      const fetchSomeData = async () => {
        // 模拟异步请求
        return { a: 1 };
      };
      const data = await fetchSomeData();
      return {
        'virtual-foo': `export default ${JSON.stringify(data)}`,
      };
    },
  };
}

这样你就可以在运行时组件中使用 virtual-foo 模块了:

import myData from 'virtual-foo';

export function MyComponent() {
  return <div>{myData.a}</div>;
}
提醒

该 hook 在 routeGenerated 之后执行。