ant-design / ant-design-pro

👨🏻‍💻👩🏻‍💻 Use Ant Design like a Pro!
https://pro.ant.design
MIT License
36.51k stars 8.15k forks source link

🧐 ant-design-pro v4.5 可能是全网最简洁的实现动态路由和动态菜单解决方案 #8121

Closed dealdot closed 3 years ago

dealdot commented 3 years ago

🧐 ant design pro v4 实现路由,菜单完全由后端返回,不在router.js中配置

估计大家有跟我一样从Vue转过来React使用 ant design pro v4 想实现动态路由,动态菜单的,即不在router.js中配置需要动态变化的路由和菜单信息, 由于ant design的封装比较厉害,实现起来不像Vue那样简单直接,看了很多issue, 结合自己的思路实现了一版,由于这一版的ant design pro V4 支持 patchRoutes,主要就是利用这一点,但我绕开了这个函数,暴力修改routes, 参考了 [#4691] (https://github.com/ant-design/ant-design-pro/issues/4691) @liangyongrui

© 版本信息

"name": "ant-design-pro", "version": "4.5.0", "umi": "^3.2.14", 浏览器环境 chrome 开发环境 windows10

💻 示例代码,稍后贴上 BasicLayout.jsx 代码,代码已贴出,见评论区

k983551019 commented 3 years ago

坐等大佬升到V5

chenxiongeee commented 3 years ago

坐等大佬发出代码

comeFromMars commented 3 years ago

坐等大佬发出代码

cc7gs commented 3 years ago

占个位置

dealdot commented 3 years ago

~~来了来了,让大家久等了 ^_^

1 我的router.js

export default [
  {
    path: '/',
    //这里的路径是相对于pages目录下
    component: '../layouts/BlankLayout',
    routes: [
      {
        path: '/',
        component: '../layouts/SecurityLayout',
        routes: [
          {
            path: '/',
            component: '../layouts/BasicLayout',
            routes: [
              //动态从服务器获取并动态加载
            ]
          }
        ],
      },
    ],
  },
];

2 后端返回的路由格式

{
    "path": "/",
    "component": "ReportPublish/ReportPublish",
    "name": "发布报表",
    "redirect": "",
    "meta": {
      "icon": "smile"
    },
 },
 {
    "path": "/report/finance",
    "component": "Layout",
    "name": "财务部",
    "redirect": "/report/finance/salary/id/1",
    "meta": {
      "icon": "heart"
     },
    "children": [
      {
        "path": "/report/finance/id/111",
        "component": "ReportManage/ReportDetail",
        "name": "工资组所有人可看报表",
        "redirect": "",
        "meta": {
          "icon": "heart"
        },
      },
      {
        "path": "/report/finance/salary",
        "component": "Layout",
        "name": "工资组",
        "redirect": "/report/finance/salary/id/1",
        "meta": {
          "icon": "heart"
        },
        "children": [
          {
            "path": "/report/finance/salary/id/1",
            "component": "ReportManage/ReportDetail",
            "name": "1月工资报表",
            "redirect": "",
            "meta": {
              "icon": "heart"
            },
          },
          {
            "path": "/report/finance/salary/id/2",
            "component": "ReportManage/ReportDetail",
            "name": "2月工资报表",
            "redirect": "",
            "meta": {
              "icon": "heart"
            },
          }
        ]
      }
    ]
  }

3 BasicLayout.jsx 代码


import ProLayout, { DefaultFooter } from '@ant-design/pro-layout';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useIntl, connect, history } from 'umi';
import { SmileOutlined, HeartOutlined } from '@ant-design/icons';
import { Result, Button } from 'antd';
import RightContent from '@/components/GlobalHeader/RightContent';
import logo from '../assets/logo.45a21ca4.png';

const IconMap = {
  smile: <SmileOutlined />,
  heart: <HeartOutlined />,
};

const generateMenu = (item) => {
  const menu = {
    path: item.path,
    name: item.name,
    icon: IconMap[item.meta.icon],
  };
  return menu;
};

const generateRouter = (item) => {
  const router = {
    path: item.path,
    exact: true,
  };
  if (item.redirect) {
    router.redirect = item.redirect;
  }
  if (item.component !== 'Layout') {
    // eslint-disable-next-line import/no-dynamic-require
    router.component = require(`../pages/${item.component}.jsx`).default;
  }
  return router;
};

const getAsyncRouter = (asyncRouterMap) => {
  let Routers = [];
  if (asyncRouterMap && asyncRouterMap.length > 0) {
    asyncRouterMap.forEach((item) => {
      if (item.children) {
        Routers.push(generateRouter(item));
        Routers = Routers.concat(getAsyncRouter(item.children));
      } else {
        Routers.push(generateRouter(item));
      }
    });
  }
  return Routers;
};

const getRouterMenu = (asyncRouterMap) => {
  let RouterMenus = [];
  if (asyncRouterMap && asyncRouterMap.length > 0) {
    asyncRouterMap.forEach((item) => {
      const parent = generateMenu(item);
      let children = [];
      if (item.children) {
        children = getRouterMenu(item.children);
      }
      if (children.length > 0) {
        parent.children = children;
      }
      RouterMenus.push(parent);
    });
  }
  return RouterMenus;
};

const defaultFooterDom = (
  <DefaultFooter copyright={`${new Date().getFullYear()} xx科技(上海)有限公司`} links={[]} />
);

const BasicLayout = (props) => {
  console.log(props)
  const { asyncRouters, children, settings } = props;
  const reactRouters = getAsyncRouter(asyncRouters);
  useEffect(() => {
    reactRouters.forEach((item) => {
      props.route.routes.push(item);
    });
    // eslint-disable-next-line no-irregular-whitespace
    if (reactRouters.length > 0) {
      props.route.routes.push({ component: require('../pages/404.jsx').default });
    }
    // fix bug: 刷新浏览器对应的path不会加载component
    history.push(window.location.pathname);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [reactRouters.length]);

  return (
    <ProLayout
      logo={logo}
      {...props}
      {...settings}
      onMenuHeaderClick={() => history.push('/')}
      menuItemRender={(menuItemProps, defaultDom) => {
        return <Link to={menuItemProps.path}>{defaultDom}</Link>;
      }}
      breadcrumbRender={(routers = []) => [...routers]}
      footerRender={() => defaultFooterDom}
      route={{ routes: getRouterMenu(asyncRouters) }}
      rightContentRender={() => <RightContent />}
    >
      {children}
    </ProLayout>
  );
};

export default connect(({ settings, asyncrouter }) => ({
  asyncRouters: asyncrouter.asyncRouter,
  settings,
}))(BasicLayout);

5 主要逻辑

1 要区分菜单与路由的区别

菜单 在ant design pro v4.5 只有两个属性 path 和 name 就可以确定菜单了

比如把以下数组传给proLayout组件的 route 属性就可以在侧边展示一个发布报表的菜单了,最多再加一个icon,则展示相应icon

[
  {
     "path": "/",
     "name": "发布报表"
   }
]

路由 信息 在ant design pro v4.5 只有两个属性 path 和 component 就可以确定相应path(路由)对应的component了, 最多再添加一个redirect属性

因此现在要做的事情就是用户登录成功后获取后端动态路由(菜单)信息,然后把 相应的 path 和name给到proLayout渲染出菜单,再把相应的path和component对应信息动态挂载到BasicLayout组件的props.route.routes上,即暴力修改这个routes就可以了,当然要注意一点,这样的话刷新浏览器对应的path不会加载到对应的component了,需要以下bug fix

// fix bug: 刷新浏览器对应的path不会加载component history.push(window.location.pathname);

另外我这个动态菜单不是在BasicLayout.jsx中dispatch请求的,而是把请求过的数据直接指定给ProLayout的route属性,这样个人感觉好一些,比较简洁,并且不用要多写个loading 来fix 渲染menu的问题

cc7gs commented 3 years ago

你的做法与 #4691 不同,原文通过在路由中不配置path并缓存 compoents,最后根据接口信息 unShift 路由信息。 所以你的做法存在如下问题:

@dealdot
最后建议将代码写一份到 codesanbox,利于浏览

chenshuai2144 commented 3 years ago

格式化一下代码吧

by05021 commented 3 years ago

安装你的代码实现了,路由权限管理,现在有个问题就是不知道为啥子菜单的icon不显示,大佬你的正常吗

qiurenbo commented 3 years ago

动态路由的意义何在

dealdot commented 3 years ago

安装你的代码实现了,路由权限管理,现在有个问题就是不知道为啥子菜单的icon不显示,大佬你的正常吗

and design 默认就不让显示子菜单icon