ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Umi 运行时配置(Runtime Config)完全指南:从 src/app.tsx 到插件化配置体系

Umi 运行时配置(Runtime Config)完全指南:从 src/app.tsx 到插件化配置体系 Umi 运行时配置Runtime Config完全指南从 src/app.tsx 到插件化配置体系【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi运行时配置Runtime Config是 Umi 中与编译期配置config并列的另一套配置体系二者最核心的区别在于运行时配置跑在浏览器端。因此你可以在这里写函数、写 tsx、import浏览器端依赖但注意不要引入 node 依赖。本篇指南将以 docs/docs/docs/api/runtime-config.md 为骨架结合仓库源码讲解src/app.tsx的约定、defineApp的类型提示、以及getInitialState、patchClientRoutes、rootContainer、onRouteChange等全部内置运行时配置项读完你可以独立完成登录鉴权、埋点统计、动态路由、全局 Provider 注入等实战改造。一、什么是运行时配置Umi 有两类配置编译期配置写在config/config.ts或.umirc.ts中作用于构建过程例如路由生成、插件启用、webpack 选项等。运行时配置写在src/app.tsx中随应用一起打包并在浏览器端执行用于在应用启动、路由切换、渲染等时机注入自定义逻辑。因为运行时配置是浏览器端代码所以它可以导出函数如onRouteChange、render、rootContainer直接写 tsx / JSX 元素引入任何浏览器端依赖如react、antd、业务请求库。注意不要在这里importnode 依赖如fs、path否则会打爆浏览器 bundle 或直接构建失败。二、配置方式约定src/app.tsxUmi 约定src/app.tsx为运行时配置文件支持app.ts/app.tsx/app.jsx/app.js等扩展名。从源码看Umi 通过正则/(\/|\\)app.(ts|tsx|jsx|js)$/来识别运行时配置文件的默认导出见 packages/preset-umi/src/features/tmpFiles/tmpFiles.tsconst appPluginRegExp /(\/|\\)app.(ts|tsx|jsx|js)$/;也就是说你在src/app.tsx中既可以export default {}整体导出也可以按具名导出单个配置项如export function patchClientRoutes(...)。Umi 会把src/app.tsx当作第一个运行时插件加载其导出成员会与各插件注册的运行时配置 key 合并、逐一调用。三、TypeScript 提示使用defineApp想让写配置时也有完整的类型提示可以使用 Umi 提供的defineApp方法定义配置有两种写法import { defineApp } from umi; export default defineApp({ layout: () { return { title: umi, }; }, }); // or 具名导出单个配置项 import { RuntimeConfig } from umi; export const layout: RuntimeConfig[layout] () { return { title: umi, }; };defineApp的底层实现可以在模板 packages/preset-umi/templates/defineApp.tpl 中看到它只是把一个RuntimeConfig原样返回的“类型锚点”函数export function defineApp(config: RuntimeConfig): RuntimeConfig { return config; }RuntimeConfig类型的生成逻辑在 packages/preset-umi/src/features/tmpFiles/tmpFiles.ts先基于内置的IDefaultRuntimeConfig接口定义默认运行时配置类型再遍历所有插件目录下生成的runtimeConfig.d.ts即RUNTIME_TYPE_FILE_NAME将各插件的IRuntimeConfig通过交叉类型合并进来export type RuntimeConfig IDefaultRuntimeConfig; // ... runtimeConfigType Plugin${pluginIndex};这就是为什么你开启某个插件后defineApp的提示会自动多出该插件的运行时配置项——类型是插件化、按需合并的。内置的IDefaultRuntimeConfig接口见 defineApp.tpl包含以下默认运行时配置 keyinterface IDefaultRuntimeConfig { onRouteChange?: (props: { routes: any, clientRoutes: any, location: any, action: any, isFirst: boolean }) void; patchRoutes?: (props: { routes: any }) void; patchClientRoutes?: (props: { routes: any }) void; render?: (oldRender: () void) void; rootContainer?: (lastRootContainer: JSX.Element, args?: any) void; modifyServerLoaderRequest?: (memo: { url: string, options: RequestInit }, args: { id: string, basename?: string }) { url: string, options: RequestInit }; [key: string]: any; }而运行时插件的调用顺序也在这里定义addRuntimePluginKey的 initialValue见 tmpFiles.tspatchRoutes → patchClientRoutes → modifyContextOpts → modifyClientRenderOpts → rootContainer → innerProvider → i18nProvider → accessProvider → dataflowProvider → outerProvider → render → onRouteChange → modifyServerLoaderRequest这个顺序决定了各配置项在应用启动链路中的执行时机。四、配置项详解以下配置项按字母排序与官方文档一致。dva如果你使用了 dva可以配置 dva 插件的运行时配置例如export default { dva: { immer: true, extraModels: [], }, };从 packages/plugins/src/dva.ts 的类型声明可以看到dva运行时配置实际支持两个维度export interface IRuntimeConfig { dva?: { config?: { initialState?: Recordstring, any; onError?: any; onStateChange?: any; onAction?: any; onHmr?: any; onReducer?: any; onEffect?: any; extraReducers?: any; extraEnhancers?: any; [key: string]: any; }, plugins?: string[]; } }即dva.config可透传 dva 实例的各类钩子与中间件onError、onStateChange、onAction、extraReducers、extraEnhancers等dva.plugins用于追加 dva 插件。除了这两个维度还有一个文档中直接给出的便捷写法extraModelsType:string[]Default:[]作用配置额外的 dva model除了约定目录自动扫描之外的 model 文件。immerType:boolean | objectDefault:false作用是否启用 immer 以方便修改 reducer。注如需兼容 IE11需配置{ immer: { enableES5: true }}。数据流若你需要定义初始化数据、跨页面共享状态可以使用getInitialState、useModel等数据流相关功能。有两种开启方式方式一推荐创建自带数据流功能的umijs/max项目详见 Umi max 简介。方式二手动开启安装umijs/plugins并在.umirc.ts中注册数据流相关插件pnpm add -D umijs/plugins// .umirc.ts export default { plugins: [ umijs/plugins/dist/initial-state, umijs/plugins/dist/model, ], initialState: {}, model: {}, };其中initial-state插件负责getInitialState与全局初始状态model插件提供useModel。getInitialStateType:getInitialState: () PromiseDataType extends any | anygetInitialState()的返回值将成为全局初始状态。例如// src/app.ts import { fetchInitialData } from /services/initial; export async function getInitialState() { const initialData await fetchInitialData(); return initialData; }现在各种插件和你定义的组件都可以通过useModel(initialState)直接获取到这份全局的初始状态import { useModel } from umi; export default function Page() { const { initialState, loading, error, refresh, setInitialState } useModel(initialState); return {initialState}/; }useModel(initialState)返回的对象属性对象属性类型介绍initialStateany导出的getInitialState()方法的返回值loadingbooleangetInitialState()或refresh()方法是否正在进行中。在首次获取到初始状态前页面其他部分的渲染都会被阻止errorError如果导出的getInitialState()方法运行时报错报错的错误信息refresh() void重新执行getInitialState方法并获取新的全局初始状态setInitialState(state: any) void手动设置initialState的值手动设置完毕会将loading置为false源码级原理解读initial-state插件见 packages/plugins/src/initial-state.ts通过api.addRuntimePluginKey(() [getInitialState])注册运行时 key并在onGenerateFiles阶段生成initialState.ts这一内置 model该 model 内部用useState维护{ initialState, loading, error }refresh会重新执行getInitialState()成功则写入initialState失败则写入errorsetInitialState支持直接传值或传更新函数(prev) next赋值完成后loading置为false同时生成Provider.tsx在首次获取到初始状态loading !appLoaded.current之前渲染Loading组件可通过initialState.loading配置指定自定义 loading 组件路径这解释了文档中“首次获取到初始状态前阻止页面其他部分渲染”的行为。layoutType:RuntimeConfig | ProLayoutProps修改内置布局的配置比如配置退出登录、自定义导航暴露的渲染区域等。注意需要开启layout插件见 api/config.md 中的layout配置才能使用它的运行时配置。import { RuntimeConfig } from umi; export const layout: RuntimeConfig { logout: () {}, // do something };更多具体配置参考 插件文档。从 packages/plugins/src/layout.ts 的实现可以看到布局运行时配置会被pluginManager.applyPlugins收集后透传给 ProLayout支持rightContentRender自定义右侧内容如用户头像/操作区、logout退出登录回调、noFound、notFound、unAccessible、noAccessible、childrenRender等能力并且注释中明确userConfig runtimeConfig即运行时配置的优先级高于编译期配置。onRouteChangeType:(args: { routes: Routes; clientRoutes: Routes; location: Location; action: Action; basename: string; isFirst: boolean }) void在初始加载和路由切换时做一些事情。场景一埋点统计export function onRouteChange({ location, clientRoutes, routes, action, basename, isFirst, }) { bacon(location.pathname); }场景二根据路由设置标题import { matchRoutes } from umi; export function onRouteChange({ clientRoutes, location }) { const route matchRoutes(clientRoutes, location.pathname)?.pop()?.route; if (route) { document.title route.title || ; } }源码级原理解读onRouteChange的调用发生在渲染器 packages/renderer-react/src/browser.tsx 中初始加载时以isFirst: true立即调用一次随后通过history.listen(onRouteChange)监听路由变化每次切换都会携带routes打平路由、clientRoutes树状客户端路由、location、action、basename、isFirst六个参数触发所有注册了该 key 的运行时插件。patchRoutesType:(args: { routes: Routes; routeComponents }) voidexport function patchRoutes({ routes, routeComponents }) { console.log(patchRoutes, routes, routeComponents); }routes打平的路由列表。routeComponents路由对应的组件映射。注如需动态更新路由建议使用patchClientRoutes()否则你可能需要同时修改routes和routeComponents两份数据。patchClientRoutesType:(args: { routes: Routes; }) void修改被 react-router 渲染前的树状路由表接收内容同useRoutesreact-router 的 Hooks API。这是运行时动态改路由最常用的入口。在最前面添加一个/foo路由import Page from /extraRoutes/foo; export function patchClientRoutes({ routes }) { routes.unshift({ path: /foo, element: Page /, }); }在最前面添加一个重定向路由import { Navigate } from umi; export const patchClientRoutes ({ routes }) { routes.unshift({ path: /, element: Navigate to/home replace /, }); };添加一个嵌套路由import Page from /extraRoutes/foo; export const patchClientRoutes ({ routes }) { routes.push({ path: /group, children: [{ path: /group/page, element: Page /, }], }); };与render配合请求服务端根据响应动态更新路由let extraRoutes; export function patchClientRoutes({ routes }) { // 根据 extraRoutes 对 routes 做一些修改 patch(routes, extraRoutes); } export function render(oldRender) { fetch(/api/routes) .then((res) res.json()) .then((res) { extraRoutes res.routes; oldRender(); }); }注意直接修改routes数组即可不需要返回值。与patchRoutes的取舍在运行时插件的执行顺序中patchRoutes先于patchClientRoutes执行见上文addRuntimePluginKey的顺序且patchClientRoutes操作的是最终交给 react-router 渲染的树状结构可以只维护一份数据因此官方建议动态路由优先使用patchClientRoutes。qiankunUmi 内置了qiankun插件来提供微前端的能力具体参考 插件配置。开启后src/app.tsx中可配置qiankun运行时配置例如export const qiankun { // 主应用相关配置如 routes 匹配、props 下发 // 或子应用相关配置如 defer、props };子应用可通过运行时配置声明挂载时机、主应用下发 props主应用可配置routes、microAppProps等详见 micro-frontend 文档。对应的运行时类型定义在 packages/plugins/src/qiankun/master.ts 的IRuntimeConfig中。renderType:(oldRender: Function) void覆写 render在真正渲染之前做异步操作如权限校验、远程配置拉取操作完成后必须调用oldRender()才能让应用继续渲染。export function render(oldRender) { fetch(/api/auth).then(auth { if (auth.isLogin) { oldRender() } else { location.href /login; oldRender() } }); }注意上面示例中else分支在跳转登录页后也调用了oldRender()——如果登录页本身也需要渲染这种写法是可行的如果你的登录页与应用是两个独立入口则可在跳转后不调用oldRender()来阻止应用渲染。render与rootContainer、patchClientRoutes的执行顺序见上文运行时插件 key 的注册顺序render位于rootContainer之后、onRouteChange之前。request如果你使用了import { request } from umi;来请求数据那么你可以通过该配置来自定义中间件、拦截器、错误处理适配等。具体参考 request 插件配置典型的运行时配置形如import { requestConfig } from umi; export const request: requestConfig { timeout: 1000, errorConfig: { errorHandler() {}, errorThrower() {}, }, requestInterceptors: [], responseInterceptors: [], };rootContainerType:(container: JSX.Element, args: { routes: Routes; plugin; history: History }) JSX.Element修改交给 react-dom 渲染时的根组件常用于在外面包一层 Providerexport function rootContainer(container, args) { return React.createElement(ThemeProvider, null, container); }args包含routes全量路由配置plugin运行时插件机制pluginManagerhistoryhistory 实例。源码级原理解读在渲染器 packages/renderer-react/src/browser.tsx 中rootContainer初始值为React.createElement(Routes, ...)随后通过opts.pluginManager.applyPlugins({ key: rootContainer, type: modify, initialValue: rootContainer })按注册顺序层层包裹最终把包裹后的结果交给ReactDOM.createRoot(...).render(...)。因此多个插件/配置项的rootContainer会形成“洋葱式”嵌套后执行的包在最外层。五、更多配置插件可自由注册运行时配置Umi 允许插件注册运行时配置如果你使用了插件一定能在插件里找到更多运行时的配置项。插件的运行时配置声明方式是生成runtimeConfig.d.tsRUNTIME_TYPE_FILE_NAME再被合并进全局RuntimeConfig类型见上文 tmpFiles.ts 的类型合并逻辑。仓库中已实现运行时配置的插件包括但不限于antdantd运行时配置如dark、compact、configProvider等dva上文介绍的dva.config、dva.pluginsinitial-stategetInitialStatelayoutlayout运行时配置localelocale运行时配置如getLocale、setLocale相关逻辑qiankun微前端运行时配置requestrequest中间件与拦截器配置。六、小结与最佳实践运行时配置核心用途关键时机/顺序getInitialState全局初始状态配合useModel(initialState)使用应用启动时首次获取前阻止页面渲染layout修改内置布局退出登录、右侧内容区等需要开启 layout 插件onRouteChange埋点统计、动态设置标题初始加载 每次路由切换patchRoutes修改打平路由与组件映射早于 patchClientRoutespatchClientRoutes动态增删改路由推荐渲染前直接改数组即可qiankun微前端主/子应用配置需开启 qiankun 插件render渲染前异步操作鉴权、远程配置在 rootContainer 之后调用request请求中间件、拦截器、错误处理需使用import { request } from umirootContainer全局包裹 Provider洋葱式嵌套后注册在外层几个实践要点区分两类配置构建期改config/config.ts浏览器期逻辑函数、tsx、浏览器依赖一律放src/app.tsx并避免引入 node 依赖。善用defineApp开启插件后类型会自动合并扩展配合编辑器可以获得完整的参数提示。动态路由优先patchClientRoutes它操作的是最终交给 react-router 的树状路由只需维护一份数据。注意执行顺序运行时插件按patchRoutes → patchClientRoutes → rootContainer → render → onRouteChange的顺序执行理解这个顺序才能正确处理跨配置项的联动例如render中拉取数据、patchClientRoutes中基于数据改路由。通过以上配置项的组合你可以覆盖应用启动初始化、权限控制、埋点统计、动态路由、全局状态、微前端接入等绝大多数浏览器端定制需求。【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进