在 umi + Ant Design Pro 中实现从服务端加载菜单

200 阅读2分钟

引言

在接触若依系统时,发现菜单是通过服务端获取的。作为一名前端开发者,我之前一直使用 Ant Design Pro,因此对“如何在 Ant Design Pro 中实现服务端菜单”产生了兴趣。

经过调研和实践,我最终实现了一个简单的服务端菜单渲染方案,过程中也踩了一些坑。在这里总结一下,希望能帮助到有类似需求的小伙伴。

实现步骤

环境版本

  • Ant Design Pro v6
  • umi v4.4.10

服务端返回的菜单数据结构(示例)

[  {    "id": "1",    "name": "系统管理",    "parentId": "0",    "path": "/system",    "component": "",    "routeName": "system",    "icon": "SettingOutlined",    "children": [      {        "id": "2",        "name": "用户管理",        "parentId": "1",        "path": "/system/user",        "component": "/system/user/index",        "routeName": "user",        "children": []
      }
    ]
  }
]

核心代码(src/app.tsx)

import React from 'react';
import type { RuntimeConfig } from '@umijs/max';
import AuthWrapper from './wrappers/auth';

// 服务端菜单数据结构
type Menu = {
  id: string;
  name: string;
  parentId: string;
  path: string;
  component: string;
  query: string | null;
  routeName: string;
  type: 'D' | 'M' | 'F';
  icon: string | null;
  children: Menu[];
};

// 替代静态 wrappers 的方式
function withWrapper(Element: React.ReactNode) {
  return <AuthWrapper>{Element}</AuthWrapper>;
}

let extraRoutes: Menu[] | undefined;

/**
 * 在运行时动态修改路由,把服务端菜单合并进来
 */
export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = ({ routes }) => {
  if (!extraRoutes) return;

  const mergeRoutes = (menus: Menu[]): any =>
    menus.map((menu) => ({
      path: menu.path,
      name: menu.routeName,
      icon: menu.icon ? <IconRender icon={menu.icon} /> : undefined,
      element: menu.component
        ? withWrapper(React.createElement(React.lazy(() => import(`@/pages${menu.component}`))))
        : undefined,
      children: menu.children?.length ? mergeRoutes(menu.children) : undefined,
    }));

  // 必须挂载到 / 布局路由下,否则菜单不会显示
  const layoutRoute = routes.find((r: any) => r.path === '/');
  if (layoutRoute && Array.isArray(layoutRoute.children)) {
    layoutRoute.children.push(...mergeRoutes(extraRoutes));
  }
};

/**
 * 应用渲染前拉取服务端菜单
 */
export const render: RuntimeConfig['render'] = (oldRender) => {
  getCurrentUserMenuTree().then((res) => {
    extraRoutes = res.data.data as Menu[];
    oldRender();
  });
};

图标渲染(可选:src/components/IconRender.tsx)

import React from 'react';
// 建议不要整体引入,会增加打包体积,只引入会用到的图标
import { UserOutlined } as Icons from '@ant-design/icons';

const Icons = {
    UserOutlined,
};

export default function IconRender({ icon }: { icon?: string | null }) {
  if (!icon) return null;
  const Cmp = (Icons as any)[icon];
  return Cmp ? React.createElement(Cmp) : null;
}

常见坑点与建议

  • 菜单必须合并到 / 布局路由:否则 ProLayout 不会渲染菜单。
  • 组件路径要与 @/pages 下的实际文件一致:注意首个斜杠与大小写。
  • 图标名称需与 @ant-design/icons 导出的组件名一致:如 SettingOutlined、UserOutlined。
  • 懒加载与权限包装:React.lazy + 自定义 withWrapper 可替代静态 wrappers。

结语

感谢您的阅读!这是我首次在掘金分享技术经验,文章中可能有不够完善的地方,欢迎大家批评指正。希望我的经验能对您在实际项目中有所帮助,也期待与大家交流更多心得。