一、整体架构
┌─────────────────────────────────────────────────────────────┐
│ 主应用 (Ant Design Pro) │
├─────────────────────────────────────────────────────────────┤
│ 1. Mock数据 → 提供动态路由配置 │
│ 2. getInitialState → 获取并存储路由数据 │
│ 3. transformDynamicRoutes → 转换路由格式 │
│ 4. patchClientRoutes → 动态注入路由 │
│ 5. layout.menu.request → 动态生成菜单 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 微应用 (qiankun) │
├─────────────────────────────────────────────────────────────┤
│ • sub-ops-tool (端口 9001) │
│ • 其他子应用... │
└─────────────────────────────────────────────────────────────┘
二、核心文件及代码
1. Mock 数据接口
// mock/subApplication.ts
import type { Request, Response } from 'express';
export default {
'GET /api/micro-app/routes': (req: Request, res: Response) => {
res.json({
code: 200,
data: [
{
path: '/application/ops-tool',
microApp: 'sub-ops-tool',
name: '运维工具',
exact: true,
},
{
path: '/application/test',
name: '测试',
component: '@/pages/application/test',
exact: true,
},
],
});
},
};
2. 动态路由转换工具
// src/utils/dynamicRoutes.tsx
import React, { lazy, ReactElement } from 'react';
import { MicroApp } from '@umijs/max';
// 本地组件映射表
const localComponentMap: Record<string, React.ComponentType> = {
'@/pages/application/test': lazy(() => import('@/pages/application/test')),
};
// 微应用加载组件
const MicroAppLoader: React.FC<{ name: string }> = ({ name }) => {
return (
<div style={{ padding: 24, minHeight: '100%' }}>
<MicroApp name={name} />
</div>
);
};
// 默认占位组件
const DefaultPlaceholder: React.FC<{ path: string }> = ({ path }) => (
<div style={{ padding: 24, textAlign: 'center' }}>
<h2>页面开发中</h2>
<p>路由路径:{path}</p>
</div>
);
/**
* 将动态路由配置转换为 Umi 路由对象
* @param routes 原始路由配置
* @returns Umi 路由对象数组
*/
export const transformDynamicRoutes = (routes: any[]): any[] => {
return routes.map(route => {
const { path, name, microApp, component, exact = true } = route;
let element: ReactElement | null = null;
if (microApp) {
// 微应用路由
element = React.createElement(MicroAppLoader, { name: microApp });
} else if (component && localComponentMap[component]) {
// 本地组件路由(使用 Suspense 支持 lazy 加载)
const Component = localComponentMap[component];
element = React.createElement(
React.Suspense,
{ fallback: React.createElement('div', { style: { padding: 24 } }, '加载中...') },
React.createElement(Component)
);
} else {
// 默认占位
element = React.createElement(DefaultPlaceholder, { path });
}
return { path, element, name, exact };
});
};
3. 主应用配置 (app.tsx)
// src/app.tsx
import { transformDynamicRoutes } from '@/utils/dynamicRoutes';
import { request as originRequest } from '@umijs/max';
// ==================== 全局状态 ====================
export async function getInitialState() {
let microAppRoutes = [];
// 获取动态路由配置
try {
const { data } = await originRequest('/api/micro-app/routes');
microAppRoutes = data || [];
} catch (err) {
console.error('获取动态路由失败:', err);
}
// 获取用户信息
const fetchUserInfo = async () => {
// ... 用户信息逻辑
};
return {
currentUser: await fetchUserInfo(),
microAppRoutes, // 存储动态路由配置
settings: defaultSettings,
};
}
// ==================== 动态路由注入 ====================
export function patchClientRoutes({ routes }: { routes: any[] }) {
// 递归查找 /application 路由
const findAppRoute = (items: any[]): any => {
for (const item of items) {
if (item.path === '/application') return item;
if (item.routes) {
const res = findAppRoute(item.routes);
if (res) return res;
}
}
return null;
};
const appRoute = findAppRoute(routes);
if (!appRoute) return;
// 获取动态路由配置并转换
const microRoutes = (window as any)?.g_initialState?.microAppRoutes || [];
const dynamicRoutes = transformDynamicRoutes(microRoutes);
// 初始化并去重添加
if (!appRoute.routes) appRoute.routes = [];
const existingPaths = new Set(appRoute.routes.map((r: any) => r.path));
const newRoutes = dynamicRoutes.filter((r: any) => !existingPaths.has(r.path));
appRoute.routes.push(...newRoutes);
appRoute.children = appRoute.routes;
}
// ==================== 动态菜单生成 ====================
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
return {
menu: {
request: async (params, menuData) => {
// 找到 application 菜单并添加子菜单
const addMenus = (items: any[]) => {
for (const item of items) {
if (item.path === '/application') {
const microRoutes = initialState?.microAppRoutes || [];
const dynamicMenus = microRoutes.map((route: any) => ({
path: route.path,
name: route.name,
}));
if (!item.children) item.children = [];
item.children.push(...dynamicMenus);
break;
}
if (item.children) addMenus(item.children);
}
};
addMenus(menuData);
return menuData;
},
},
// ... 其他布局配置
};
};
// ==================== qiankun 微应用配置 ====================
export const qiankun = {
master: {
apps: [
{
name: 'sub-ops-tool',
entry: '//localhost:9001',
base: '/application/ops-tool',
},
],
sandbox: true,
prefetch: true,
},
};
// ==================== 挂载全局状态 ====================
export async function render(oldRender: () => void) {
const state = await getInitialState();
(window as any).g_initialState = state;
oldRender();
}
4. 路由配置
// config/routes.ts
export default [
// ... 其他路由
{
path: '/application',
name: 'application',
component: './application/index', // 布局组件
routes: [], // 留空,动态注入
},
// ... 其他路由
];
5. 布局组件
// src/pages/application/index.tsx
import { Outlet } from '@umijs/max';
import { PageContainer } from '@ant-design/pro-components';
const ApplicationLayout: React.FC = () => {
return (
<PageContainer>
<Outlet /> {/* 必须有 Outlet 才能渲染子路由 */}
</PageContainer>
);
};
export default ApplicationLayout;
6. 测试页面组件
// src/pages/application/test/index.tsx
import React from 'react';
const TestPage: React.FC = () => {
return (
<div style={{ padding: 24 }}>
<h1>✅ 测试页面</h1>
<p>动态路由注入成功!</p>
</div>
);
};
export default TestPage;
三、执行流程图
用户访问 /application/test
│
▼
┌───────────────────────────────────────────────────────┐
│ 1. 页面加载 │
│ ├── render() 执行 │
│ ├── getInitialState() 请求 /api/micro-app/routes │
│ └── 存储到 window.g_initialState.microAppRoutes │
└───────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────┐
│ 2. 路由注入 │
│ ├── patchClientRoutes() 执行 │
│ ├── 查找 /application 路由 │
│ ├── transformDynamicRoutes() 转换格式 │
│ │ ├── 微应用 → MicroApp 组件 │
│ │ ├── 本地组件 → lazy() + Suspense │
│ │ └── 默认 → 占位组件 │
│ └── 追加到 appRoute.routes │
└───────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────┐
│ 3. 菜单生成 │
│ ├── layout.menu.request() 执行 │
│ ├── 查找 /application 菜单 │
│ ├── 添加动态子菜单项 │
│ └── 返回完整菜单数据 │
└───────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────┐
│ 4. 页面渲染 │
│ ├── ApplicationLayout 渲染(包含 Outlet) │
│ ├── 匹配 /application/test 路由 │
│ └── 渲染 TestPage 组件 │
└───────────────────────────────────────────────────────┘
四、关键技术点总结
核心概念
| 概念 |
说明 |
重要性 |
| element vs component |
UmiJS 4+ 使用 element (React Element) 而非 component (组件类) |
⭐⭐⭐⭐⭐ |
| React.createElement |
将组件转换为 React Element |
⭐⭐⭐⭐⭐ |
| Outlet |
父路由必须包含 Outlet 才能渲染子路由 |
⭐⭐⭐⭐⭐ |
| Suspense |
lazy 加载的组件必须用 Suspense 包裹 |
⭐⭐⭐⭐ |
| patchClientRoutes |
UmiJS 运行时修改路由的钩子 |
⭐⭐⭐⭐⭐ |
| getInitialState |
初始化全局状态,获取动态数据 |
⭐⭐⭐⭐ |
常见问题及解决方案
| 问题 |
原因 |
解决方案 |
| 页面空白 |
路由使用 component 而非 element |
改用 React.createElement() 创建 element |
| 子路由不显示 |
父组件缺少 <Outlet /> |
在布局组件中添加 <Outlet /> |
| 组件找不到 |
lazy 导入路径错误 |
检查路径格式 @/pages/xxx |
| 微应用不加载 |
微应用未启动或配置错误 |
确认微应用运行在指定端口 |
| 菜单不显示 |
未在 menu.request 中添加 |
在 layout.menu.request 中动态添加 |
| 重复注入路由 |
patchClientRoutes 多次执行 |
添加去重逻辑 (Set 检查) |
数据流向
Mock接口 ──► getInitialState ──► window.g_initialState
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
patchClientRoutes layout.menu.request 其他组件
(注入路由) (生成菜单) (使用数据)
│ │
▼ ▼
路由系统渲染 侧边栏菜单
五、最佳实践建议
1. 路由配置规范
// ✅ 正确
{
path: '/application/test',
element: React.createElement(Component),
name: '测试',
}
// ❌ 错误
{
path: '/application/test',
component: Component, // UmiJS 4 不支持
}
2. 去重处理
const existingPaths = new Set(appRoute.routes.map(r => r.path));
const newRoutes = dynamicRoutes.filter(r => !existingPaths.has(r.path));
3. 错误处理
try {
const { data } = await fetchRoutes();
return data;
} catch (err) {
console.error('获取路由失败:', err);
return []; // 返回空数组作为降级方案
}
4. 类型定义
interface DynamicRoute {
path: string;
name?: string;
microApp?: string;
component?: string;
exact?: boolean;
}
interface UmiRoute {
path: string;
element: React.ReactElement;
name?: string;
exact?: boolean;
}
六、文件结构参考
project/
├── config/
│ ├── config.ts # 主配置
│ └── routes.ts # 路由配置
├── mock/
│ └── subApplication.ts # Mock 数据
├── src/
│ ├── app.tsx # 运行时配置(核心)
│ ├── utils/
│ │ └── dynamicRoutes.tsx # 路由转换工具
│ └── pages/
│ └── application/
│ ├── index.tsx # 布局组件
│ └── test/
│ └── index.tsx # 测试页面
└── sub-ops-tool/ # 微应用
└── ...
七、快速排查清单
这份笔记涵盖了完整的实现过程和关键要点,后续遇到类似问题可以参考排查。