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/              # 微应用
    └── ...

七、快速排查清单

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

posted @ 2026-04-22 16:51  土豆儿哥  阅读(57)  评论(0)    收藏  举报