Umi 内置功能插件开发指南:手把手为 preset-umi 添加一个 Feature Plugin
2026/9/14 21:55:01 网站建设 项目流程

Umi 内置功能插件开发指南:手把手为 preset-umi 添加一个 Feature Plugin

【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi

本文基于 Umi 官方仓库的 TAKUMI.md 技术文档展开,从零讲解如何为 Umi 框架新增一个内置功能插件(built-in feature plugin):包括插件文件的编写规范、api.describe的配置描述方法、在preset-umi中的注册流程,并结合仓库真实源码(以 404 插件为范本)剖析插件从"注册"到"生效"的底层机制。读完本文,你将掌握在packages/preset-umi/src/features/下独立开发一个可被用户通过配置开启的内置功能的完整方法论。

一、背景:什么是 Umi 的内置功能插件

Umi 是 React 社区的一个可扩展前端应用框架,其核心设计哲学是"一切皆插件"。框架能力(如 Mock、SSR、MPA、代码分割、Polyfill、404 路由兜底等)并不是硬编码在核心里的,而是通过preset-umi 插件集以功能插件(feature plugin)的形式逐个注册进 Umi Service,最终在编译期、dev 期和构建期发挥作用。

从 preset-umi 入口文件 可以看到,Umi 的整个功能体系就是一张庞大的插件清单:

  • 404(路由兜底)
  • aiDevdevToolterminal(开发体验)
  • mockapiRoute(数据模拟与接口)
  • ssrmpaexportStatic(构建形态)
  • webpackvitemakoswc(构建器与编译器)
  • tmpFilesclientLoaderrouteProps(临时文件与路由能力)

本文要讲的就是:当你想为 Umi 新增一项内置能力时,应该如何做。这正是 TAKUMI.md 这份文档的核心内容,它用简洁的两步流程给出了官方推荐的标准姿势。

二、第一步:编写插件文件${name}.ts

2.1 目录与文件命名约定

按照 TAKUMI.md 的规范,每个内置功能插件独占一个目录,目录名与文件名同名(均使用插件的name):

packages/preset-umi/src/features/${name}/${name}.ts

例如,404 插件对应的真实路径是packages/preset-umi/src/features/404/404.ts,mock 插件位于packages/preset-umi/src/features/mock/mock.ts,SSR 插件位于packages/preset-umi/src/features/ssr/ssr.ts。完整的插件目录清单可直接查看 features 目录。

2.2 插件文件的最小骨架

TAKUMI.md 给出的最小可运行示例是一个api.describe调用:

import { IApi } from '../../types'; export default (api: IApi) => { api.describe({ key: '404', }); };

这里有两个关键点需要理解:

  1. IApi类型:Umi 插件接收的唯一入参就是 Service 暴露的插件 API 对象api,其类型IApi定义在packages/preset-umi/src/types.ts中。IApi是 Umi Core 的PluginAPI(见packages/core/src/service/pluginAPI.ts)与 preset-umi 自定义能力的合并类型,涵盖了生命周期、配置修改、路由、中间件、临时文件等全套钩子。
  2. api.describe:用于描述插件的 key(配置键名)、配置项 schema、默认值和启用方式。它必须在插件注册阶段执行,是插件能被 Umi 正确识别、校验与启用的前提。

__sample.ts是仓库中一份更简化的空插件模板,位于 packages/preset-umi/src/features/__sample.ts,可作为你复制改写的起点:

import { IApi } from '../types'; export default (api: IApi) => { api; };

2.3 深入api.describe:让插件可配置、可按需启用

api.describe的实现位于 packages/core/src/service/pluginAPI.ts,其完整签名如下:

describe(opts: { key?: string; config?: IPluginConfig; enableBy?: EnableBy | ((enableByOpts: { userConfig: any; env: Env }) => boolean); }) { // default 值 + 配置开启冲突,会导致就算用户没有配 key,插件也会生效 if (opts.enableBy === EnableBy.config && opts.config?.default) { throw new Error( `[plugin: ${this.plugin.id}] The config.default is not allowed when enableBy is EnableBy.config.`, ); } this.plugin.merge(opts); }

官方插件 API 文档(docs/docs/docs/api/plugin-api.md)对各个字段做了详细说明,整理如下:

字段含义注意事项
key该插件在 Umi 配置中的键名,例如key: '404'对应配置文件中的404: {}用户可通过配置该键来控制插件
config.default插件配置的默认值;当用户没有在配置中写该 key 时默认配置生效enableBy: EnableBy.config互斥,同时使用时pluginAPI.ts会直接抛错,避免"没配 key 插件也生效"的歧义
config.schema基于 joi 的配置类型声明,例如(joi) => joi.string()如果你希望用户进行配置,这个是必须的,否则用户配置不会被校验也不会生效
config.onChangedev 模式下配置被修改后的处理机制默认api.ConfigChangeType.reload(重启 dev 进程);可改为api.ConfigChangeType.regenerateTmpFiles(仅重新生成临时文件);也可传入自定义方法
enableBy插件的启用方式默认api.EnableBy.register(注册即启用);改为api.EnableBy.config后,只有用户配置了该插件的 key 才启用;也可以传一个返回布尔值的函数实现动态启用

一个带完整配置描述的示例(摘自插件 API 文档):

api.describe({ key: 'foo', config: { schema(joi) { return joi.string(); }, onChange: api.ConfigChangeType.regenerateTmpFiles, }, enableBy: api.EnableBy.config, });

EnableBy枚举在插件 API 文档中被明确列出,包含registerconfig两种取值,分别对应"注册即启用"与"配置才启用"两种插件激活策略。

三、第二步:将插件注册到 preset-umi

编写完插件文件后,还需要把它挂载到 Umi 的插件系统中。按照 TAKUMI.md 的说明,在 packages/preset-umi/src/index.ts 的features部分追加一行require.resolve即可。

该文件的整体结构如下(节选):

export default () => { return { plugins: [ // registerMethods require.resolve('./registerMethods'), // features process.env.DID_YOU_KNOW !== 'none' && require.resolve('@umijs/did-you-know/dist/plugin'), require.resolve('./features/404/404'), require.resolve('./features/aiDev/aiDev'), // ... 其余 features ].filter(Boolean), }; };

假设你的插件名是foo,注册方式就是:

require.resolve('./features/foo/foo'),

几点值得注意:

  1. require.resolve而非直接import:所有插件都以绝对路径字符串形式交给 Umi Service 解析加载,这是 Umi 插件机制(支持本地插件、npm 包插件统一管理)的基础。
  2. 路径无扩展名./features/foo/foo会自动解析到foo.ts,与目录内单文件约定保持一致。
  3. filter(Boolean):列表中混有process.env.DID_YOU_KNOW !== 'none' && ...这类条件表达式(返回false时被过滤),因此新注册的插件直接写进数组即可,无需特殊处理。
  4. registerMethods是特殊的首个插件:它在 packages/preset-umi/src/registerMethods.ts 中通过循环批量注册了大量通用方法(如addHTMLLinksaddBeforeMiddlewaresonGenerateFiles等生命周期钩子),是后续所有 feature 插件可以调用这些 API 的前提。

3.1 注册顺序的工程考量

在 index.ts 中,部分插件并非简单平铺,而是带有注释说明的有序依赖

// 1. generate tmp files require.resolve('./features/tmpFiles/tmpFiles'), // 2. `clientLoader` and `routeProps` depends on `tmpFiles` files require.resolve('./features/clientLoader/clientLoader'), require.resolve('./features/routeProps/routeProps'), // 3. `ssr` needs to be run last require.resolve('./features/ssr/ssr'),

这表明:当你的新插件依赖某些既有插件(例如需要操作tmpFiles生成的临时文件,或需要在 SSR 管线中挂接逻辑)时,必须把require.resolve放在对应插件之后注册。Umi 插件按注册顺序执行生命周期钩子,顺序即依赖。

四、源码剖析:以 404 插件为例看一个真实 feature 的完整形态

TAKUMI.md 示例中的key: '404'在仓库中有真实对应的完整实现,位于 packages/preset-umi/src/features/404/404.ts:

import { IApi, IRoute } from '../../types'; type Routes = Record<string, IRoute>; export function patchRoutes(routes: Routes): Routes { Object.keys(routes).forEach((key) => { if (routes[key].path === '404') { routes[key].path = '*'; routes[key].absPath = '/*'; } }); return routes; } export default (api: IApi) => { api.describe({ key: '404', }); api.modifyRoutes(async (routes: Routes) => { // 仅支持约定式路由 if (api.config.routes) { return routes; } return patchRoutes(routes); }); };

这个例子清晰地展示了"最小骨架 → 真实功能"的演进路径:

  1. api.describe({ key: '404' }):仅声明 key,不配置 schema 与 enableBy,因此它采用默认的EnableBy.register注册即启用——这也是为什么 Umi 项目天然拥有*通配兜底路由的原因。
  2. api.modifyRoutes:注册一个路由修改钩子,将约定式路由中path === '404'的路由改写成*通配路由,实现未匹配页面的兜底。
  3. api.config.routes守卫:当用户显式配置了routes(配置式路由)时直接返回、不干预,体现了 feature 插件与用户配置之间的协作边界。

这个模式具有普适性:describe声明身份,生命周期钩子(modifyRoutesonGenerateFilesaddHTMLMetas等)执行逻辑,api.config读取用户配置决定行为分支。你在开发自己的内置插件时,完全可以照搬这套结构。

五、开发内置功能插件的完整检查清单

结合 TAKUMI.md 的流程与仓库实际实现,一个合格的内置功能插件需要满足:

  1. 文件位置正确packages/preset-umi/src/features/${name}/${name}.ts,与其它插件平级。
  2. 默认导出插件函数export default (api: IApi) => { ... }IApi../../types导入(注意__sample.ts因位于 features 根目录使用../types,子目录内统一为../../types)。
  3. 调用api.describe:至少声明key;如需用户可配置,必须提供config.schema(否则用户配置无效)并谨慎选择enableByconfig.default的搭配。
  4. 注册进插件清单:在packages/preset-umi/src/index.tsplugins数组中追加require.resolve('./features/${name}/${name}'),注意依赖顺序。
  5. 遵循插件 API 契约:所有可用钩子、方法、类型详见 插件 API 文档,包括api.describeEnableByConfigChangeTypeServiceStage等核心概念,以及registerMethods.ts中注册的各类addXxx/onXxx方法。

六、结语

从 TAKUMI.md 的两步流程出发,到 404 插件 的真实实现、pluginAPI.ts 中describe的底层校验逻辑,再到 preset-umi 入口 中数十个 feature 的注册范式,可以看到 Umi 的内置功能插件体系遵循着高度一致的约定:

一个目录 + 一个文件 + 一次describe+ 一行require.resolve

这种约定使得框架能力的扩展成本极低——新增一个内置功能,本质上就是往这个标准化流水线上追加一个模块。理解了这个流程,你不仅能自主为 Umi 添加内置能力,也能更深刻地理解 Umi 如何以"插件清单"的方式组织起从路由、构建到开发体验的全部功能,而这正是 Umi 保持可扩展性的核心所在。

【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询