
Umi 插件开发实战指南从机制原理、生命周期到自定义插件编写【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umiUmi 的核心能力源于其插件机制通过插件开发者可以扩展项目的编译时与运行时能力实现修改打包配置、改造启动流程、约定目录结构、定制 HTML 输出等丰富功能。本文基于 Umi 官方插件指南文档见 docs/docs/docs/guides/plugins.md并结合仓库源码packages/core、packages/preset-umi深入讲解插件机制的底层实现帮助读者掌握插件的核心概念、启用/禁用/配置方式以及api的完整工作机理最终能够独立编写高质量插件。核心概念插件的本质就是一个函数插件的本质是一个接收api参数的方法函数。在插件中你可以调用api提供的各种方法来注册 hook随后 Umi 会在特定的生命周期时机执行这些 hook。这一设计在源码中体现为packages/core/src/service/plugin.ts中Plugin.apply会加载插件文件并返回其默认导出ret.__esModule ? ret.default : ret该导出即插件函数。以文档中的changeFavicon插件为例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; }); };这个插件的作用是根据用户配置的changeFavicon值来修改配置中的favicons。可以看到插件就是一个接收参数api的方法我们通过api.describe声明插件的key、配置 schema 和启用方式通过api.modifyConfig注册一个(memo) {...}的 hook。当用户在配置中填写changeFavicon后Umi 会注册该插件在 Umi 收集配置的生命周期resolveConfigstage里注册的 hook 被执行配置中的favicons即被改写为用户配置的changeFavicon。值得指出的是上述插件声明中config.schema基于 joi 描述配置类型如果希望用户能够配置该插件schema是必须的否则用户的配置不生效。enableBy: api.EnableBy.config表示配置启用即只有用户配置了changeFavicon项时插件才被启用详见后文启用插件。plugin 与 preset 的区别preset的作用是预设一批插件它通常用来注册一组 presets 和 plugins。与 plugin 不同的是preset 的函数可以有返回值返回值是包含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 放在队首的目的在于保证 presets 之间的顺序和关系是可控的。从源码看这一逻辑位于packages/core/src/service/service.ts的initPreset与initPlugin方法preset 返回的 presets 通过opts.presets.unshift(...)插入队首返回的 plugins 通过opts.plugins.push(...)追加到队尾。另一个关键点在 plugin 中你也可以 return 一些 plugins 或 presets但 Umi 不会对它们做任何处理——service.ts中initPlugin仅对type preset的插件解析返回值而对type plugin的插件直接断言assert(!ret, plugin should return nothing)即 plugin 不允许有返回值。插件的 id 和 key每个插件都对应一个id和keyid插件所在路径的简写作为插件的唯一标识key用于插件配置的键名。例如插件node_modules/umijs/plugin-foo/index.js通常它的id是umijs/plugin-fookey是foo此时开发者可以在配置中配置键名为foo的项来对该插件进行配置。源码层面的 id/key 生成规则见packages/core/src/service/plugin.ts的getId与getKey方法id的生成优先级为包入口取pkg.name→ 项目内路径./relative/path形式→ 包内文件pkgName/path形式→ 绝对路径且umijs/preset-umi/lib/plugins前缀会被替换为key由包名去除umijs/、umi-等非 Umi 作用域前缀再剥离-plugin-/-preset-标识后转换而来非包插件则取文件名去掉扩展名。内置的preset-umi中许多插件就通过api.describe({ key: xxx })显式声明 key。启用插件插件有两种启用方式环境变量中启用和配置中启用。注意这里的插件指第三方插件Umi 的内置插件统一在配置中通过对其 key 进行配置来启用。与umi3不同Umi 4 不再支持对package.json中依赖项插件实现自动启用packages/core/src/service/plugin.ts中getPluginsAndPresets内相关的 dependencies 扫描逻辑已被注释掉。通过环境变量启用可以通过环境变量UMI_PRESETS和UMI_PLUGINS注册额外插件$ UMI_PRESETS foo/preset.js umi dev注意项目里不建议使用此方式它通常用于基于 Umi 的框架二次封装。从源码看getPluginsAndPresets会读取${prefix}_PRESETS/${prefix}_PLUGINS环境变量prefix默认即UMI按逗号分隔后解析为插件路径。通过配置启用在配置里通过presets和plugins配置插件配置的内容为插件的路径export default { presets: [./preset/foo,bar/presets], plugins: [./plugin, require.resolve(plugin_foo)] }从源码Plugin.getPluginsAndPresets可以看到插件的最终来源顺序是opts传入框架层→ 环境变量 → 用户配置每个路径都会经过resolve.sync解析支持.tsx/.ts/.mjs/.jsx/.js扩展名解析失败会抛出Invalid plugin ${path}, can not be resolved错误。插件的注册顺序Umi 插件的注册遵循一定顺序所有的 presets 都先于 plugins 被注册内置插件 → 环境变量中的插件 → 用户配置中的插件同时注册同一个数组里的插件按顺序依次注册preset 中注册的 preset 立即执行注册的 plugin 最后执行。这一顺序在service.ts的run方法中体现得很清晰initPresets阶段用while (presets.length)循环注册 presetspreset 返回的 presets 被 unshift 到队首立即执行收集到的 presetPlugins 被plugins.unshift(...presetPlugins)合并随后进入initPlugins阶段用while (plugins.length)依次注册 plugins。禁用插件有两种方式禁用插件配置 key 为 falseexport default{ mock: false }这将会禁用 Umi 内置的 mock 插件。底层实现见service.ts的isPluginEnable方法if (this.userConfig[key] false) return false;和if (this.config[key] false) return false;会在 hook 执行前直接将其过滤掉。在插件中禁用其他插件可通过api.skipPlugins(pluginId[])的方式禁用详见插件 API。skipPlugins接收插件 key 的数组packages/core/src/service/pluginAPI.ts中其实现在service.skipPluginIds集合中记录被跳过的插件 idisPluginEnable会优先检查if (this.skipPluginIds.has(id)) return false;。同时该 API 也做了安全断言插件不能跳过自己且只能跳过已注册插件的 key。查看插件注册情况命令行$ umi plugin list该命令的实现位于 packages/preset-umi/src/commands/plugin.ts它遍历api.service.plugins并对本地插件./plugin.ts/./plugin.js标注(from local)、对umijs/preset开头的内置插件标注(from preset)其余插件直接打印其 id。命令注册本身也演示了api.registerCommand的用法——插件通过registerCommand提供 CLI 功能。配置插件通过配置插件的 key 来配置插件export default{ mock: { exclude: [./foo] } }这里mock就是 Umi 内置 mock 插件的 key。mock插件的实际实现见 packages/preset-umi/src/features/mock/mock.ts它通过api.describe({ key: mock, config: { schema({ zod }) {...} }, enableBy() {...} })声明配置 schema 与启用条件仅dev命令默认开启且可通过环境变量MOCKnone关闭并通过api.onStart监听 mock 目录变化、api.addMiddlewares注入 mock 中间件。再比如我们安装一个插件umi-plugin-bar其 key 默认是bar就可以配置export default{ bar: { ... } }插件 key 的默认命名规则如果插件是一个包key 的默认值将是去除前缀的包名。比如umijs/plugin-foo的 key 默认为fooalipay/umi-plugin-bar的 key 默认为bar。值得注意的是该默认规则要求包名符合 Umi 插件的命名规范packages/core/src/service/plugin.ts中RE正则要求包名形如(umijs/|umi-)plugin-或(umijs/|umi-)preset-isPluginOrPreset用于判断。如果插件不是一个包key 的默认值将是插件的文件名。比如./plugins/foo.js的 key 默认为foo对应getKey中basename(this.path, extname(this.path))。为了避免不必要的麻烦建议为自研插件显式地声明其 key通过api.describe({ key: xxx })。Umi 插件的机制及其生命周期原文档在此配有一张Umi 插件机制示意图外部图床链接此处不再引用核心流程可用以下生命周期完整描述。各阶段与packages/core/src/service/service.ts中run方法的执行代码一一对应init stage该阶段 Umi 加载各类配置信息。包括加载.env文件loadEnvrequirepackage.json获取pkg与pkgPath加载用户的配置信息new Config(...)得到userConfigresolve 所有的插件内置插件、环境变量、用户配置依次进行见Plugin.getPluginsAndPresets。initPresets stage该阶段 Umi 注册 presets。presets 在注册时可以通过return { presets, plugins }添加额外插件。其中 presets 被添加到 presets 队列的队首presets.unshift(...)plugins 被添加到 plugins 队列的队尾plugins.push(...)。initPlugins stage该阶段 Umi 注册 plugins包括上个阶段由 presets 添加的额外 plugins。值得注意尽管 plugins 也可以return { presets, plugins }但 Umi 不会对其进行任何操作plugin 返回非空会被断言拦截。插件的 init 其实就是执行插件的代码——插件代码的本质只是调用 api 进行各种 hook 的注册hook 的执行并非在此阶段因此这里叫插件的注册。resolveConfig stage该阶段 Umi 整理各插件对config schema的定义收集configSchemas、configDefaults、configOnChanges然后执行插件的modifyConfig、modifyDefaultConfig、modifyPaths等 hook进行配置的收集。resolveConfig方法中modifyConfig的初始值是用户配置strict模式下会先按 schema 强校验modifyDefaultConfig的初始值是各插件声明的默认配置最终this.config lodash.merge(defaultConfig, config)合并出最终配置。collectionAppData stage该阶段 Umi 执行modifyAppDatahook维护 App 的元数据appData是umi4新增的 api。初始 appData 包含cwd、pkg、pkgPath、plugins、userConfig、config、defaultConfig等基础信息。onCheck stage该阶段 Umi 执行onCheckhook。onStart stage该阶段 Umi 执行onStarthook。runCommand stage该阶段 Umi 运行当前 cli 要执行的 command例如umi dev就执行 dev command。Umi 的各种核心功能都在 command 中实现包括插件调用 api 注册的绝大多数 hook。以上就是 Umi 插件机制的整体流程。插件运行过程中还会记录每个 hook 的执行耗时plugin.time.hooks配合--profilePlugins参数可以输出各插件注册与 hook 的耗时统计见service.ts的_profilePlugins。register()、registerMethod()以及applyPlugins()register()接收一个 key 和一个 hookfn、可选的before/stage它维护了一个key-hook[]的 map每当调用register()就会为 key 额外注册一个 hook。源码见packages/core/src/service/hook.tsHook 类与pluginAPI.ts的register方法push 到service.hooks[key]。register()注册的 hooks 供applyPlugins使用这些 hook 的执行顺序参照 tapablepackages/core/src/service/service.ts中applyPlugins使用AsyncSeriesWaterfallHook/SyncWaterfallHook实现支持stage与before调整顺序。registerMethod()接收一个 key 和一个 fn它会在 api 上注册一个方法。如果没有传入 fnregisterMethod()会在 api 上注册一个注册器它将register()传入 key 并柯里化后的结果作为 fn 注册到 api 上。这样通过调用这个注册器就可以快捷地为 key 注册 hook 了。registerMethod的具体实现见packages/core/src/service/pluginAPI.ts注册的方法保存在service.pluginMethods[name]未传 fn 时默认函数内部调用this.register({ key: name, ... })。packages/preset-umi/src/registerMethods.ts中通过循环[onGenerateFiles, onBeforeCompiler, onBuildComplete, addMiddlewares, addRuntimePlugin, chainWebpack, modifyWebpackConfig, modifyRoutes, modifyHTML, ...]等数十个名称逐一调用api.registerMethod({ name })这就是我们常用的api.addXxx/api.modifyXxx/api.onXxx系列 API 的来源。关于这些 API 的具体参数与示例可完整查阅插件 API 文档。PluginAPI 的原理Umi 会为每个插件赋予一个PluginAPI对象这个对象引用了插件本身和 Umi 的 service。Umi 对PluginAPI对象的get()方法进行了Proxy代理见pluginAPI.ts的proxyPluginAPI静态方法具体规则如下pluginMethod如果 prop 是 Umi 维护的pluginMethods[]通过registerMethod()注册的方法中的方法则返回这个方法service props如果 prop 是serviceProps数组中的属性如appData、args、config、cwd、pkg、pkgPath、name、paths、userConfig、env、isPluginEnable等这些是 Umi 允许插件直接访问的属性则返回 service 对应的属性函数会自动 bind 到 servicestatic props如果 prop 是参数staticProps数组中的属性静态变量如ApplyPluginsType、ConfigChangeType、EnableBy、ServiceStage等类型定义和常量则将其返回否则返回 api 自身的属性。因此Umi 提供给插件的 api 绝大多数都是依靠registerMethod()实现的可以直接使用这些 api 快速在插件中注册 hook。这也是 Umi 将框架和功能解耦的体现Umi 的 service 只提供插件的管理功能而 api 都依靠插件来提供。preset-umi内置插件集umi-core即packages/core提供了一套插件的注册及管理机制而 Umi 的核心功能都靠preset-umi来实现入口见 packages/preset-umi/src/index.ts其中require.resolve了注册方法、数十个 features 与 commands 插件。preset-umi其实就是内置的一个插件集它提供的插件分为三大类registerMethods这类插件注册了上述提到的注册器供开发者快速注册 hook这类方法占据了 PluginAPI 中的大多数见 packages/preset-umi/src/registerMethods.tsfeatures这类插件为 Umi 提供各种特性例如 appData、lowImport、mock 等如 packages/preset-umi/src/features/mock/mock.tscommands这类插件注册了各类 command提供 Umi CLI 的各种功能如 packages/preset-umi/src/commands/plugin.ts 注册的umi plugin list以及 build、dev、config、setup、generators 系列等。Umi 能够在终端中正常运行依靠的就是 command 提供的功能。编写一个完整插件流程与建议综合以上机制编写一个可发布、可配置的 Umi 插件通常遵循以下步骤创建插件文件如plugin.ts或发布为符合umijs/plugin-*/umi-plugin-*命名规范的 npm 包默认导出一个接收api: IApi的函数用api.describe声明元信息key建议显式声明、config.schema基于 joi声明配置类型用户可配置则必填、config.default默认值注意enableBy: EnableBy.config时不允许同时配置default源码中有显式断言、config.onChangedev 下配置变更的处理默认reload重启 dev 进程可改为regenerateTmpFiles或自定义函数、enableByEnableBy.register注册即启用 /EnableBy.config配置启用 / 自定义函数动态判断在合适的生命周期注册 hook如api.modifyConfig/modifyDefaultConfig改配置api.modifyWebpackConfig/modifyViteConfig/chainWebpack改打包api.modifyHTML/addHTMLHeadScripts改 HTMLapi.addEntryImports/addEntryCode改入口api.addRuntimePlugin加运行时插件api.onStart/onGenerateFiles/onBuildComplete挂接各阶段事件api.registerCommand注册 CLI 命令api.registerGenerator注册微生成器在配置文件中启用通过presets/plugins字段配置插件路径再通过插件的 key 进行具体配置。umi plugin list可用于随时核对插件是否注册成功。需要完整 API 清单与参数说明时请查阅插件 API 文档。总结Umi 的插件机制是约定 分层设计的典范packages/core的 Service 只负责插件的注册与调度register/registerMethod/applyPlugins构成 hook 的核心循环所有能力都通过 preset-umi 等插件以注册器 hook的形式注入api而api则通过 Proxy 动态分发pluginMethods、service 属性、静态常量与自身属性。理解这套机制后无论是写一个十几行的配置增强小插件还是基于 Umi 二次封装一套完整框架preset 模式都能做到心中有数、手到擒来。【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考