当前位置: 首页 > news >正文

Ant Design Pro + UmiJS 动态菜单/路由实现笔记

Ant Design Pro + UmiJS 动态菜单/路由实现笔记

一、整体架构

┌─────────────────────────────────────────────────────────────┐
│                        主应用 (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/              # 微应用└── ...

七、快速排查清单

这份笔记涵盖了完整的实现过程和关键要点,后续遇到类似问题可以参考排查。

http://www.jsqmd.com/news/682865/

相关文章:

  • 从公式到代码:拆解PyTorch中xavier_normal_的每一行,理解Glorot初始化的设计哲学
  • Real-Anime-Z效果展示:写实级皮肤毛孔+动漫级大眼比例的平衡实现
  • 3个步骤从零开始获取全国高铁数据:探索Parse12306的自动化数据采集之旅
  • 四层模块化架构重构:ComfyUI-Impact-Pack如何革新AI图像精细化处理工作流
  • 告别性能损耗:实测双路E5+GTX1060在PVE虚拟机直通后的游戏与渲染表现
  • json ignore反序列化?_?JSON反序列化时忽略字段的json----标签使用方法
  • JDBC数据库技术
  • 架构演进2026:分布式多机协同梯控中的边缘计算与云端调度设计
  • UI自动化测试(Python+selenium)
  • 如何轻松永久保存你的微信聊天记录:完整数据备份指南
  • 深度解析ACadSharp:5大核心模块掌握专业级CAD数据处理.NET库
  • Phi-3.5-mini-instruct效果展示:跨语言理解能力——中英混输准确识别与响应
  • 【Lammps】从零构建二维Ar原子体系:核心建模命令详解与脚本拆解
  • 长沙高端入户门服务商推荐|梵赫建材12年深耕更靠谱 - 中媒介
  • 零售电商如何解决商品详情页Word公式粘贴的SEO优化?
  • 保姆级教程:在N32G430上用FreeRTOSv202212.01点灯,我踩过的5个坑都帮你填好了
  • egergergeeert FLUX.1-dev提示词工程:如何用最少词汇触发最丰富视觉表达
  • 如何实现Windows系统级输入模拟:Interceptor完整指南
  • 终极指南:如何用JKSM轻松备份和管理3DS游戏存档
  • VibeVoice实时语音合成系统评测:轻量级模型,专业级效果
  • ArcGIS Pro小技巧:一键生成VTPK矢量切片包,自定义你的专属地图样式
  • 贵州安亿顺废旧物资回收:靠谱的贵阳废旧电脑回收企业推荐 - LYL仔仔
  • PCB设计避坑指南:用Allegro做无盘设计时,别忘了检查这个间距规则!
  • 别再到处找激活码了!一个批处理文件搞定Visio Professional 2019激活(附常见乱码解决方案)
  • 别再只盯着EMD了!用Python手把手实现LMD分解轴承故障信号(附完整代码)
  • LeetCode 744. 寻找比目标字母大的最小字母 技术解析
  • 避坑指南:用STM32CubeMX配置MODBUS从机时,串口DMA和HAL库回调函数那些容易踩的‘坑’
  • 从BeanHandler到MapListHandler:一文搞懂Apache DBUtils的8种ResultSetHandler,附实战代码对比
  • 2026思正工业听诊器:多场景适用+性价比高,让每家企业都能轻松拥有智能“听觉” - 品牌种草官
  • 从‘命令未找到’到GPU状态尽在掌握:nvidia-smi环境变量配置全攻略