完全指南:从 `src/app.tsx` 到浏览器端插件机制)
Umi 运行时配置Runtime Configuration完全指南从src/app.tsx到浏览器端插件机制【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi导读运行时配置Runtime Configuration是 Umi 在浏览器端执行的一组配置约定与构建期的.umirc.ts/config/config.ts配置互补。本文将以 runtime-config.en-US.md 为核心骨架逐项拆解dva、数据流getInitialState/useModel、layout、onRouteChange、patchRoutes、patchClientRoutes、qiankun、render、request、rootContainer等运行时配置项并结合 preset-umi 与 plugins 的真实源码讲清这些配置在 Umi 内部是如何被收集、注入与执行的。读完本文你将掌握运行时配置的完整写法、TypeScript 提示方案以及底层调用链能在自己的 Umi 项目中熟练进行登录鉴权、路由动态扩展、埋点统计与 Provider 包裹等实战操作。运行时配置与构建期配置的区别核心区别构建期配置运行在 Node 端服务于打包与编译运行时配置运行在浏览器端服务于应用启动与页面生命周期。由于运行时配置在浏览器端执行因此可以在其中编写函数、TSX以及导入浏览器端依赖如react、umi导出的 API切忌导入 Node 端依赖如fs、path否则会在浏览器打包阶段报错。从 umi.tpl 模板可以看到应用入口最终是这样组织的createPluginManager() → getRoutes() → patchRoutes 事件 → createHistory → render 组合 → renderClient()其中patchRoutes、render、onRouteChange、rootContainer等运行时配置正是通过插件机制pluginManager.applyPlugins在关键节点被触发的。这一点在后文各配置项中会逐一印证。配置方法约定src/app.tsxUmi 约定项目根目录下的src/app.tsx也支持src/app.ts为运行时配置文件。构建期会在tmpFiles阶段检测该文件是否存在并据此生成运行时配置的类型声明与defineApp导出。在 tmpFiles.ts 中可以看到将 defineApp.tpl 生成到core/defineApp.ts并对外导出defineApp与RuntimeConfig类型app.ts必须被最先处理源码注释明确写道app.ts should be in the first, otherwise it will be circular dependency以避免循环依赖问题。defineApp.tpl中定义的默认运行时配置接口包含以下字段interface IDefaultRuntimeConfig { onRouteChange?: (props: { routes, clientRoutes, location, action, isFirst }) void; patchRoutes?: (props: { routes }) void; patchClientRoutes?: (props: { routes }) void; render?: (oldRender: () void) void; rootContainer?: (lastRootContainer: JSX.Element, args?: any) void; modifyServerLoaderRequest?: (memo, args) ...; [key: string]: any; // 允许插件扩展任意运行时配置键 }这也解释了为什么各插件能自由注册自己的运行时配置键如dva、layout、request其类型提示由插件通过addRuntimePluginKey/RUNTIME_TYPE_FILE_NAME机制合并进RuntimeConfig联合类型。TypeScript 类型提示在编写运行时配置时可通过 Umi 导出的defineApp获得完整类型提示import { defineApp } from umi; export default defineApp({ layout: () { return { title: umi, }; }, });也可以使用RuntimeConfig索引类型按需声明单个配置项import { RuntimeConfig } from umi; export const layout: RuntimeConfig[layout] () { return { title: umi, }; };两种写法等价defineApp本质就是export function defineApp(config: RuntimeConfig): RuntimeConfig { return config; }见 defineApp.tpl只是把RuntimeConfig校验收敛到了一处。配置项详解以下配置项按字母序说明均写在src/app.tsx中导出。dva如果项目使用 dva 状态管理可通过该配置项为 dva 插件提供运行时配置详细内容参见 Plugin Configurationdva。export default { dva: { immer: true, extraModels: [], }, };extraModels类型string[]默认值[]作用配置额外的 dva models。典型场景是“按需加载”的 model——路由懒加载导致该 model 未被自动注册时将其显式加入extraModels即可。immer类型boolean | object默认值false作用是否启用 immer便于在 reducer 中直接“修改”状态immutable 更新由 immer 代理完成。注意为兼容 IE11需配置为{ immer: { enableES5: true } }。数据流Data Flow如果需要定义初始化数据可借助getInitialState、useModel等数据流功能详见 数据流文档。有两种启用方式创建umijs/max项目开箱即用数据流能力参见 Umi max 介绍手动启用提供数据流能力的插件pnpm add -D umijs/plugins// .umirc.ts export default { plugins: [ umijs/plugins/dist/initial-state, umijs/plugins/dist/model, ], initialState: {}, model: {}, };从 initial-state.ts 源码可以看到该插件的关键行为通过api.addRuntimePluginKey(() [getInitialState])注册运行时配置键getInitialState通过addExtraModels注册initialState这个内置 model生成运行时文件initialState.ts内部用useState/useEffect/useCallback实现initialState / loading / error / refresh / setInitialState的状态机同时生成Provider.tsx在首次加载完成前渲染 loading 占位if (loading !appLoaded.current typeof window ! undefined)。getInitialState类型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.ts 的实现可以看到refresh内部会先置loading: true再调用getInitialState()成功则写入initialState并结束 loading失败则写入errorsetInitialState还支持传入函数形式以基于旧值更新同时支持initialState函数式更新。layout类型RuntimeConfig | ProLayoutProps用于修改内置布局Layout的配置例如配置登出行为、自定义导航栏的暴露渲染区域等。详细配置参见 布局菜单文档 与 layout 插件运行时配置。注意需要先开启 layout 插件才能使用其运行时配置。import { RuntimeConfig } from umi; export const layout: RuntimeConfig { logout: () {}, // 自定义登出逻辑 };从 layout.ts 源码可以看出插件会通过pluginManager.applyPlugins收集所有layout运行时配置并合并到ProLayoutProps中logout会被注入到头像下拉菜单opts?.runtimeConfig?.logout?.(opts.initialState)同时rightContentRender、childrenRender、noFound、notFound、unAccessible等字段也支持运行时覆盖。onRouteChange类型(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 || ; } }参数含义routes扁平化的路由配置列表clientRoutes客户端路由树location当前路由位置对象action路由动作如POP/PUSH/REPLACEbasename应用部署的基础路径isFirst是否为首次加载其触发链路见 browser.tsxBrowserRoutes组件挂载后会立即以isFirst: true触发一次onRouteChange随后通过history.listen(onRouteChange)监听每次路由变化参数由插件管理器组装applyPlugins({ key: onRouteChange, type: event, ... })。patchRoutes类型(args: { routes: Routes; routeComponents }) voidexport function patchRoutes({ routes, routeComponents }) { console.log(patchRoutes, routes, routeComponents); }routes扁平化的路由列表routeComponents路由到组件的映射注意如需动态更新路由推荐使用patchClientRoutes()若使用patchRoutes可能需要同时修改routes和routeComponents才能保持一致。在 umi.tpl 中patchRoutes是在getRoutes()之后、创建 history 之前以event类型触发的因此它修改的是服务端/构建期生成的路由表与其组件映射。patchClientRoutes类型(args: { routes: Routes }) void在 react-router 渲染之前修改树状路由表接收的内容与 react-router 的useRoutes一致。直接在routes上修改即可无需返回值。在 browser.tsx 中createClientRoutes生成客户端路由树后会立刻以event方式触发patchClientRoutes把修改后的clientRoutes交给useRoutes渲染。在路由列表开头新增/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(); }); }qiankunUmi 内置qiankun插件提供微前端能力运行时配置请参见 微前端文档。典型场景是在主应用中通过运行时配置注册子应用、设置通信方案如qiankun.master中的routes与props子应用侧则通过qiankun.slave相关配置暴露生命周期。相关实现位于 plugins/libs/qiankun例如主应用的运行时插件masterRuntimePlugin.tsx会注册onRouteChange、qiankunMaster等运行时键。render类型(oldRender: Function) void覆盖默认渲染流程。经典用法是渲染前先做权限校验export function render(oldRender) { fetch(/api/auth).then(auth { if (auth.isLogin) { oldRender() } else { location.href /login; oldRender() } }); }注意示例中else分支在跳转登录页后仍然调用了oldRender()——这在需要保留原应用时是合理的若希望完全阻止渲染可不调用oldRender()。render在 umi.tpl 中是以compose类型执行的所有render配置会按注册顺序组合成一个洋葱模型oldRender即下一个或最终的renderClient调用。因此render可以叠加使用例如“先鉴权、再拉取动态路由、最后渲染”的多层组合。request如果使用import { request } from umi;请求数据可通过该配置自定义请求中间件、拦截器、错误处理适配器等。完整配置见 request 插件配置。典型能力包括requestInterceptors请求拦截器可在发出前注入 token、改写参数responseInterceptors响应拦截器可统一解包data、处理 401 跳转登录errorConfig错误处理适配器统一错误提示与上报自定义request方法的超时、credentials、前缀等行为。rootContainer类型(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 实例从 browser.tsx 可以清楚地看到渲染管线中各个 Provider 的优先级由低到高innerProvider → i18nProvider → accessProvider → dataflowProvider → outerProvider → rootContainerrootContainer作为modify类型的插件键拥有最高优先级即最外层包裹。因此像ThemeProvider、ConfigProvider这类全局上下文放在rootContainer中最合适而数据流 Provider如dataflowProvider则位于其内层。更多配置项由插件按需注册Umi 允许插件注册自己的运行时配置。使用插件时你会在插件中找到更多运行时配置项例如内置插件在 tmpFiles.ts 中遍历所有插件的runtimeConfig.d.ts将各插件的IRuntimeConfig与默认配置接口合并为最终的RuntimeConfig联合类型插件通过api.addRuntimePluginKey声明自己消费的运行时配置键通过api.addRuntimePlugin注入携带这些配置的运行时文件。例如initial-state插件注册了getInitialStatelayout插件注册了layout运行时配置antd插件templates/antd/runtime.ts.tpl与locale插件templates/locale/runtime.tpl分别注册antd、locale运行时配置。因此在src/app.tsx中可以依据已启用的插件自由组合使用这些运行时键并获得完整的 TypeScript 提示。小结运行时配置是 Umi 面向浏览器端的“最后一公里”定制入口与构建期配置分工明确维度构建期配置.umirc.ts/config/config.ts运行时配置src/app.tsx执行环境Node 端打包编译时浏览器端应用运行时可写内容纯配置、字符串路径函数、TSX、浏览器依赖典型用途路由声明、插件注册、构建参数鉴权、动态路由、埋点、Provider 包裹生效机制编译期静态解析插件管理器在渲染管线关键节点触发其底层统一由 Umi 插件机制驱动入口模板 umi.tpl 串联patchRoutes→render→renderClient渲染器 browser.tsx 在渲染管线中触发patchClientRoutes、onRouteChange与各 Provider 链含rootContainer。理解了这条调用链你就能在src/app.tsx中有的放矢地使用dva、getInitialState、layout、patchClientRoutes、render、rootContainer等能力构建出登录鉴权、权限路由、全局主题、埋点统计等完整的前端工程方案。【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考