Umi Plugin System: Enabling, Configuring, and Developing Plugins
【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi
Umi 的插件机制是这座 React 框架的核心:通过插件,你可以在不改动框架源码的前提下,同时扩展项目的编译期(build-time)与运行期(runtime)能力。本文以官方指南 use-plugins 为骨架,完整覆盖"如何启用插件、如何配置插件、如何编写项目级插件、如何开发自己的插件"这条完整路径,并结合当前仓库的源码实现(packages/core、packages/plugins、packages/preset-umi)深入剖析插件的注册顺序、id/key命名规则、生命周期与 PluginAPI 的工作原理。读完本文,你将能够:在标准 Umi 应用中按需启用 Max 插件、通过plugin.ts快速定制项目、并使用 Plugin API 编写可复用的 Umi 插件。
一、为什么插件是 Umi 的灵魂
官方在 Developing Plugins 中直接点明:"Umi 的本质在于它的插件机制(The essence of Umi lies in its plugin mechanism)。"基于 Umi 的插件机制,你可以增强项目的编译期与运行期能力,自由使用官方提供的 Plugin API 实现丰富的功能,例如:
- 修改代码打包配置(webpack / vite / mako)
- 修改启动代码(entry 文件)
- 约定新的目录结构
- 修改 HTML 产物
一个插件本质上就是一个接收api参数的普通函数。在函数内部,你可以调用api提供的方法来注册各种钩子(hooks),Umi 会在特定的时机执行这些钩子。
二、在标准 Umi 应用中使用插件
默认不包含任何插件
与直觉相反:在标准 Umi 应用中,默认不包含任何插件。如果希望使用 Umi Max 的功能(如数据流、antd 等),你需要手动安装插件并启用。
先安装官方插件集合@umijs/plugins:
pnpm add -D @umijs/plugins然后以启用 antd 插件为例,在配置文件(.umirc.ts或config/config.ts)中声明:
// .umirc.ts export default { plugins: ['@umijs/plugins/dist/antd'], antd: {}, }Umi 与 Max 的区别
Umi 与 Max 的核心区别在于:Max 已经内置了大部分插件,例如数据流(initial-state、model)、antd等。这些插件都可以从@umijs/plugins/dist/*加载并启用。当前仓库中@umijs/plugins的源码位于 packages/plugins/src,从中可以看到 Max 内置的插件全集,包括:
| 插件入口 | 提供能力 |
|---|---|
access | 权限访问控制 |
analytics | 站点分析 |
antd | antd 组件库集成 |
confetti | 彩带动效 |
dva | dva 状态管理 |
initial-state | 应用初始化数据 |
layout | ProLayout 布局 |
locale | 国际化 |
mf | 模块联邦 |
model | 数据流模型 |
moment2dayjs | moment 迁移 dayjs |
qiankun | 微前端 |
react-query | React Query 集成 |
request | 请求库封装 |
styled-components | CSS-in-JS |
tailwindcss | Tailwind CSS |
unocss | UnoCSS |
valtio | Valtio 状态管理 |
关于 Max 各特性的详细配置说明,请参阅 Umi Max。
💡我应该选择 Max 吗?使用 Max 并不意味着你必须使用它的全部功能,你可以按需禁用插件。因此,当你需要 Max 的某些特性时,随时可以选择创建一个 Max 项目。
三、项目级插件(plugin.ts)
如果只想在项目中快速使用插件能力(例如自定义 HTML 产物),可以在项目根目录创建plugin.ts,编写一个项目级插件,Umi 会自动将它加载为插件。
根据 Directory Structure 的说明,项目根目录的plugin.ts即项目级 Umi 插件:当你有 Umi 定制需求时,通常需要使用 Plugin API(例如修改产物 HTML),此时创建该文件即可。一个典型的plugin.ts示例:
import type { IApi } from 'umi'; export default (api: IApi) => { api.onDevCompileDone((opts) => { opts; // console.log('> onDevCompileDone', opts.isFirstCompile); }); api.modifyHTML(($) => { $; }); api.chainWebpack((memo) => { memo; }); };可以看到,项目级插件与普通插件没有任何区别——同样是一个接收api的默认导出函数。Umi 启动时会自动读取根目录下的plugin.ts并将其注册进插件队列,因此它非常适合承载"仅属于当前项目、不需要对外发布"的定制逻辑,例如调整 HTML、注入编译完成回调、修改 webpack 链式配置等。
四、开发插件:核心概念
若需要把定制能力沉淀为可复用插件,请参考 Developing Plugins。下面梳理其核心概念。
4.1 一个最小插件
插件的本质是一个接收api的方法,在方法内通过api注册钩子。以下插件的作用是:根据用户配置的changeFavicon值,修改配置中的 favicon:
import { IApi } from 'umi'; export default (api: IApi) => { api.describe({ key: 'changeFavicon', config: { schema(joi) { return joi.string(); }, }, enableBy: api.EnableBy.config, }); api.modifyConfig((memo) => { memo.favicons = api.userConfig.changeFavicon; return memo; }); };在这段代码中,api.describe声明了插件的配置信息(key、config.schema、enableBy),api.modifyConfig注册了一个(memo) => {...}钩子。当用户在配置中写了changeFavicon后,Umi 才注册该插件(EnableBy.config语义);在 Umi 收集配置的生命周期里,这个钩子被执行,从而把favicon改为用户配置的changeFavicon。
4.2 plugin 与 preset
**preset(预设)**的作用是预置一批插件,通常用于一次性注册一批 presets 和 plugins。在 preset 中,上述接收api的方法可以拥有返回值,返回值是一个包含plugins和presets属性的对象,用于注册对应的插件或插件集合:
import { IApi } from 'umi'; export default (api: IApi) => { return { plugins: ['./plugin_foo', './plugin_bar'], presets: ['./preset_foo'], }; };注册顺序值得注意:presets 永远在 plugins 之前注册。Umi 维护两个队列按顺序注册 presets 和 plugins:示例中注册的preset_foo会被放到 presets 队列的头部,而plugin_foo、plugin_bar会被依次追加到 plugins 队列的尾部。将 preset 放在队首,是为了保证 preset 之间的顺序与依赖关系可控。
另一点值得注意的是:在插件(plugin)中同样可以返回一些 plugins 或 presets,但Umi 不会对此做任何处理。
4.3 插件的 id 与 key
每个插件都对应一个id与一个key:
- id:插件路径的缩写,是插件的唯一标识;
- key:插件在配置中使用的键名。
例如插件node_modules/@umijs/plugin-foo/index.js,其id通常是@umijs/plugin-foo,key是foo。这样开发者就能在配置中用键名foo来配置该插件。
这一规则在当前仓库的 Plugin 实现 中可以得到印证。getKey()的推导逻辑是:
// e.g. // initial-state -> initialState // webpack.css-loader -> webpack.cssLoader function nameToKey(name: string) { return name .split('.') .map((part) => lodash.camelCase(part)) .join('.'); } return nameToKey( opts.isPkgEntry ? Plugin.stripNoneUmiScope(opts.pkg.name).replace(RE[this.type], '') : basename(this.path, extname(this.path)), );其中RE是识别 Umi 插件命名规范的正则:
const RE = { plugin: /^(@umijs\/|umi-)plugin-/, preset: /^(@umijs\/|umi-)preset-/, };也就是说:如果是包入口,则去掉@umijs/、umi-等作用域前缀以及plugin-/preset-前缀得到 key;如果不是包,则取文件名(去掉扩展名)作为 key,再经过 camelCase 转换。例如@alipay/umi-plugin-bar的默认 key 是bar,./plugins/foo.js的默认 key 是foo。为避免不必要的麻烦,官方建议为自己的插件显式声明 key(通过api.describe的key字段)。
id的推导见getId():包入口直接用包名;位于项目目录内则用相对路径(./xxx);否则拼接包名与相对路径。此外id还会把@umijs/preset-umi/lib/plugins替换为@@并去掉.js后缀。
五、启用插件
Umi 4 中插件有两种启用方式:环境变量启用与配置启用。(与umi@3不同,Umi 4 不再支持自动启用package.json依赖中以@umijs/preset-、@umijs/plugin-、umi-preset-、umi-plugin-开头的插件/预设,这一点在 directory-structure 中也有明确说明。若需自定义插件/预设,必须手动在配置中声明。)
注意:这里讨论的是第三方插件,Umi 内置插件普遍通过配置中的 key 来启用。
5.1 环境变量启用
可以通过环境变量UMI_PRESETS和UMI_PLUGINS注册额外的插件:
$ UMI_PRESETS=foo/preset.js umi dev官方特别提醒:这种方式不推荐在项目中使用,通常用于基于 Umi 框架做二次封装(如脚手架 CLI)的场景。
5.2 配置启用
在配置中通过presets与plugins字段启用插件,配置的内容是插件的路径:
export default { presets: ['./preset/foo', 'bar/presets'], plugins: ['./plugin', require.resolve('plugin_foo')], };源码层面的收集顺序在 getPluginsAndPresets 中可以看到:依次合并命令行 opts 传入的 presets/plugins → 环境变量(UMI_PRESETS/UMI_PLUGINS)→ 用户配置中的presets/plugins,再逐个resolve.sync解析路径(支持.tsx/.ts/.mjs/.jsx/.js扩展名)并构造Plugin实例。
5.3 插件的注册顺序
Umi 插件的注册遵循一定顺序:
- 所有 presets 都在 plugins 之前注册;
- 内置插件 → 环境变量中的插件 → 用户配置中的插件;
- 同一时刻(同一数组中)注册的插件按顺序注册;
- preset 中注册的 presets 立即执行,而注册的 plugins 在最后执行。
六、禁用插件
禁用插件有两种方式:
6.1 将 key 配置为 false
export default { mock: false, };这样会禁用 Umi 内置的 mock 插件。
6.2 在插件中禁用其他插件
通过api.skipPlugins(pluginId[])实现,详见 Plugin API。skipPlugins接收插件 key 的数组,源码实现(pluginAPI.ts)会校验:不能跳过自己、被跳过的 key 必须已被某个插件注册,随后把对应插件的 id 加入service.skipPluginIds集合。
七、查看插件注册状态
通过命令行查看当前注册了哪些插件:
$ umi plugin list八、配置插件
通过插件的 key 来配置插件:
export default { mock: { exclude: ['./foo'] }, };这里mock是 Umi 内置 mock 插件的 key。再比如安装一个插件umi-plugin-bar,其默认 key 是bar,则可以这样配置:
export default { bar: { ... }, };默认命名规则小结
- 若插件是包,默认 key 为去掉前缀后的包名:
@umijs/plugin-foo→foo,@alipay/umi-plugin-bar→bar(前提是包名符合 Umi 插件命名规范); - 若插件不是包,默认 key 为插件文件名:
./plugins/foo.js→foo; - 推荐显式声明 key,避免歧义。
九、插件机制与生命周期
Umi 的插件机制整体运行过程如下(生命周期状态在 types.ts 中以ServiceStage枚举定义,各阶段依次推进):
| 阶段 | 作用 |
|---|---|
init | 加载各种配置信息:加载.env文件、读取package.json、加载用户配置,并按序解析所有插件(内置插件、环境变量、用户配置) |
initPresets | 注册 presets。preset 可通过返回{ presets, plugins }注册更多插件:presets 追加到 presets 队列头部,plugins 追加到 plugins 队列尾部 |
initPlugins | 注册插件(含上一阶段 preset 追加的插件)。注意:插件即使返回{ presets, plugins },Umi 也不会处理。插件 init 的本质是执行插件代码——而插件代码只是在调用 api 注册各种钩子,钩子此时并不会执行,因此这一阶段被称为"插件注册" |
resolveConfig | 汇总各插件声明的config schema,然后执行modifyConfig、modifyDefaultConfig、modifyPaths等钩子收集配置 |
collectAppData | 执行modifyAppData钩子,维护 App 的元数据(AppData是umi@4新增的 api) |
onCheck | 执行onCheck钩子 |
onStart | 执行onStart钩子 |
runCommand | 运行当前要执行的 CLI 命令(如umi dev)。Umi 的各种核心功能都在命令中实现,包括我们插件注册的大部分钩子 |
9.1register()、registerMethod()与applyPlugins()
register():接收一个 key 和一个 hook,维护key → hook[]的映射。每次调用都会为同一个 key 追加注册一个 hook。注册的钩子供applyPlugins使用,执行顺序遵循 tapable 的规则。register还支持stage(默认 0,负数提前执行、正数延后执行)与before(指定在某 hook 之前执行)调整顺序。registerMethod():接收一个 name 和可选的 fn,在 api 上注册一个方法。若不传 fn,则会注册一个"注册器"到 api 上:该注册器把传入的 fn 与 name 作为 key 封装成一次register()调用。例如api.registerMethod({ name: 'addFoo' })之后,每次调用api.addFoo(fn)等价于api.register({ key: 'addFoo', fn })。源码实现(pluginAPI.ts)会把方法存入service.pluginMethods供 Proxy 取用。
applyPlugins根据类型聚合钩子的执行结果,三种类型与默认规则如下:
add:按钩子顺序把返回值拼接成数组,fn接收args,initialValue必须是数组(默认空数组)。当 key 以add开头且未显式声明 type 时默认采用此类型。modify:按钩子顺序依次修改initialValue,因此必须传initialValue。fn第一个参数是memo(前面钩子修改后的累积结果),需要返回修改后的 memo。当 key 以modify开头时默认采用此类型。event:按顺序执行,不需要initialValue,fn无需返回值。当 key 以on开头时默认采用此类型。
9.2 PluginAPI 的原理
Umi 为每个插件分配一个 PluginAPI 对象,它同时引用插件自身与 Umi 的 service。Umi 按照如下规则对 PluginAPI 对象的get()方法做了 Proxy 代理(见 proxyPluginAPI):
- pluginMethod:若属性是 Umi 维护的
pluginMethods[]中的方法(通过registerMethod()注册的),返回该方法; - service props:若属性在
serviceProps数组中(Umi 允许插件直接访问的 service 属性),返回 service 的对应属性; - static props:若属性在
staticProps数组中(静态变量,如类型定义和常量),返回它; - 否则,返回 api 自身的属性。
因此,Umi 提供给插件的大部分 api 都依赖registerMethod()实现,你可以直接使用这些 api 快速注册钩子。这也是 Umi"框架与功能解耦"的体现:Umi 的 service 只负责插件管理,而所有 api 都依赖插件来提供。
9.3 preset-umi
umi-core(即 packages/core)提供了一套插件注册与管理机制,而 Umi 的核心功能全部由 preset-umi 实现。preset-umi本质上是一个内置插件集,提供三类插件:
- registerMethods:注册前面提到的大量"注册器",供开发者快速注册钩子,占据 PluginAPI 的大部分;
- features:为 Umi 提供各种特性,如
appData、lowImport、mock等; - commands:注册各类命令,提供 Umi CLI 的各种功能。Umi 能在终端正常运行,正是依赖 commands 提供的功能。
十、从使用到开发的完整路径
总结一下,围绕"插件"这条主线,你可以按需选择使用方式:
- 标准 Umi 项目:安装
@umijs/plugins并在配置中声明plugins: ['@umijs/plugins/dist/antd']等条目,即可按需启用 Max 特性; - 项目级定制:在项目根目录创建
plugin.ts,Umi 自动加载,配合 Plugin API 快速修改 HTML、webpack 配置、开发编译回调等; - 沉淀为可复用插件/预设:编写一个导出
(api) => {}的模块,用api.describe声明 key 与配置 schema,用register/registerMethod注册钩子,遵循plugin-/preset-命名规范后即可通过配置或环境变量在任何 Umi 项目中启用; - 调试与验证:使用
umi plugin list查看插件注册状态,使用api.skipPlugins禁用不需要的插件。
无论你处于哪一步,插件的核心心智模型始终不变:插件是函数,api 是入口,钩子是时机。理解了这个模型,Umi 的绝大部分能力都在你的掌控之中。
【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考