版本信息

ant design pro : v4.2.2
umi: v3.2.14
pro-layout: v6.4.19
typescript: v4.0.3

思路

一、菜单是在在src/layouts/BasicLayout.tsx的menuDataRender属性中进行渲染,所以需要把后台获取到的数据传入menuDataRender属性
二、使用react hooks的useEffect 中使用dva的dispatch来请求菜单。

具体代码

菜单显示

src/layouts/BasicLayout.tsx

/**
 * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
 * You can view component api by:
 * https://github.com/ant-design/ant-design-pro-layout
 */
import ProLayout, {
  MenuDataItem,
  BasicLayoutProps as ProLayoutProps,
  Settings,
  DefaultFooter,
} from '@ant-design/pro-layout';
import React, { useEffect, useMemo, useRef } from 'react';
import { Link, useIntl, connect, Dispatch, history } from 'umi';
import { Result, Button } from 'antd';
import Authorized from '@/utils/Authorized';
import RightContent from '@/components/GlobalHeader/RightContent';
import { ConnectState } from '@/models/connect';
import { getMatchMenu } from '@umijs/route-utils';
import logo from '../assets/images/logo.png';

const noMatch = (
  <Result
    status={403}
    title="403"
    subTitle="Sorry, you are not authorized to access this page."
    extra={
      <Button type="primary">
        <Link to="/user/login">Go Login</Link>
      </Button>
    }
  />
);
export interface BasicLayoutProps extends ProLayoutProps {
  breadcrumbNameMap: {
    [path: string]: MenuDataItem;
  };
  route: ProLayoutProps['route'] & {
    authority: string[];
  };
  settings: Settings;
  dispatch: Dispatch;
  menuData: MenuDataItem[];
}
export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
  breadcrumbNameMap: {
    [path: string]: MenuDataItem;
  };
};
/**
 * use Authorized check all menu item
 */

// const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
//   menuList.map((item) => {
//     const localItem = {
//       ...item,
//       children: item.children ? menuDataRender(item.children) : undefined,
//     };
//     return Authorized.check(item.authority, localItem, null) as MenuDataItem;
//   });

const defaultFooterDom = (
  <DefaultFooter copyright={`${new Date().getFullYear()} 浙江网络科技有限公司`} links={false} />
);

const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
  const {
    dispatch,
    children,
    settings,
    location = {
      pathname: '/',
    },
    menuData, // 后台服务器返回的菜单数据
    loading,
  } = props;

  const menuDataRef = useRef<MenuDataItem[]>([]);

  useEffect(() => {
    if (dispatch) {
      dispatch({
        type: 'user/fetchCurrent',
      });
      dispatch({ // 调用后台请求,获取菜单
        type: 'menu/fetchMenu',
      })
    }
  }, []);

  /**
   * init variables
   */

  const handleMenuCollapse = (payload: boolean): void => {
    if (dispatch) {
      dispatch({
        type: 'global/changeLayoutCollapsed',
        payload,
      });
    }
  };
  // get children authority
  const authorized = useMemo(
    () =>
      getMatchMenu(location.pathname || '/', menuDataRef.current).pop() || {
        authority: undefined,
      },
    [location.pathname],
  );

  const { formatMessage } = useIntl();

  return (
    <ProLayout
      logo={logo}
      formatMessage={formatMessage}
      onCollapse={handleMenuCollapse}
      onMenuHeaderClick={() => history.push('/')}
      menuItemRender={(menuItemProps, defaultDom) => {
        if (menuItemProps.isUrl || !menuItemProps.path) {
          return defaultDom;
        }
        return <Link to={menuItemProps.path}>{defaultDom}</Link>;
      }}
      breadcrumbRender={(routers = []) => [
        {
          path: '/',
          breadcrumbName: formatMessage({ id: 'menu.home' }),
        },
        ...routers,
      ]}
      itemRender={(route, params, routes, paths) => {
        const first = routes.indexOf(route) === 0;
        return first ? (
          <Link to={paths.join('/')}>{route.breadcrumbName}</Link>
        ) : (
          <span>{route.breadcrumbName}</span>
        );
      }}
      footerRender={() => defaultFooterDom}
      // menuDataRender={() => menuData} // menuDataRender属性中传入菜单,这样是不对后台数据做任何处理,直接显示成菜单
      menuDataRender={() => menuDataRender(menuData)} // menuDataRender传入菜单,是后台返回的数据,经过前端鉴权后的数据。如当前登录身份为user,后台返回的菜单中有一个权限为authority,不经过处理会直接显示,而前端处理一下menuDataRender(menuData)后,这个菜单就不会显示出来。
      // menuDataRender={menuDataRender}
      menu={{
        loading,
      }}
      rightContentRender={() => <RightContent />}
      postMenuData={(menuData) => {
        menuDataRef.current = menuData || [];
        return menuData || [];
      }}
      {...props}
      {...settings}
    >
      <Authorized authority={authorized!.authority} noMatch={noMatch}>
        {children}
      </Authorized>
    </ProLayout>
  );
};

export default connect(({ global, settings, menu }: ConnectState) => ({
  collapsed: global.collapsed,
  settings,
  menuData: menu.menuData,  // connect连接menu
  loading: menu.loading,
}))(BasicLayout);

因为用的是typescript,所以需要定义connect中的menu的类型
src/models/connect.d.ts

import { MenuDataItem, Settings as ProSettings } from '@ant-design/pro-layout';
import { GlobalModelState } from './global';
import { UserModelState } from './user';
import { StateType } from './login';
import { MenuModelState } from './menu';

export { GlobalModelState, UserModelState };

export interface Loading {
  global: boolean;
  effects: { [key: string]: boolean | undefined };
  models: {
    global?: boolean;
    menu?: boolean;
    setting?: boolean;
    user?: boolean;
    login?: boolean;
  };
}

export interface ConnectState {
  global: GlobalModelState;
  loading: Loading;
  settings: ProSettings;
  user: UserModelState;
  login: StateType;
  menu: MenuModelState, // 定义menu的类型,MenuModelState是在src/models/menu.ts中定义的
}

export interface Route extends MenuDataItem {
  routes?: Route[];
}

/**
 * @type T: Params matched in dynamic routing
 */
export interface ConnectProps<T = {}> extends Partial<RouterTypes<Route, T>> {
  dispatch?: Dispatch<AnyAction>;
}

获取菜单,services, model

向后台服务器请求数据

新建src/services/menu.ts

import request from '@/utils/request'

export async function getMenuData(): Promise<any> {
  console.log('services')
  return request('/api/getMenu', {
    method: 'POST',
  })
}
菜单model

新建src/models/menu.ts

import { Effect, Reducer } from 'umi'
import { getMenuData } from '@/services/menu'
import { MenuDataItem } from '@ant-design/pro-layout'

export interface MenuModelState {
  menuData: MenuDataItem[],
  loading: boolean,
}

export interface MenuModelType {
  namespace: 'menu';
  state: MenuModelState;
  effects: {
    fetchMenu: Effect;
  };
  reducers: {
    saveMenuData: Reducer<MenuModelState>;
  };
}

const MenuModel: MenuModelType = {
  namespace: 'menu',
  state: {
    menuData: [],
    loading: true,
  },
  effects: {
    *fetchMenu(_, { call, put }) {
      const response = yield call(getMenuData)
      console.log('response:', response)
      yield put({
        type: 'saveMenuData',
        payload: response,
      })
    }
  },
  reducers: {
    saveMenuData(state, action) {
      return {
        ...state,
        menuData: action.payload || [],
        loading: false,
      }
    }
  }
}

export default MenuModel

后台数据格式

一、后台返回的数据中一定要有path, name
1、返回的数据的name可以覆盖前端写的name
二、返回的数据可以设置icon,authority
1、不管返回的数据有没有icon,前端写的icon都会无效。
2、返回的数据的authority可以覆盖前端写的authority。如果返回的数据没有authority,则前端写的authority会生效。

const mock = [
  {
    path: '/dashboard',
    name: 'dashboard',
    icon: 'dashboard',
    children: [
      {
        path: '/dashboard/analysis',
        name: 'analysis',
        exact: true,
      },
      {
        path: '/dashboard/monitor',
        name: 'monitor',
        exact: true,
      },
      {
        path: '/dashboard/workplace',
        name: 'workplace',
        exact: true,
      },
    ],
  }
]

后台返回的数据在前端项目中也还是要写的

config/routes.ts

const routes = [
    {
    path: '/',
    component: '../layouts/SecurityLayout',
    routes: [
      {
        path: '/',
        component: '../layouts/BasicLayout',
        authority: ['admin', 'user'],
        routes: [
          {
          path: '/dashboard',
          name: 'dashboard',
          icon: 'dashboard',
          component: './Welcome',
          routes: [
            {
              path: '/dashboard/analysis',
              name: 'analysis',
              exact: true,
            },
            {
              path: '/dashboard/monitor',
              name: 'monitor',
              exact: true,
            },
            {
              path: '/dashboard/workplace',
              name: 'workplace',
              exact: true,
            },
          ],
        }
        ]
      }
    ]
  }
]

export default routes;

问题描述:菜单未显示。

一、问题排查:
1、在src/layouts/BasicLayout.tsx中获取到了menuData,第一次获取到[], 第二次获取到后台返回的正确数据。但是菜单仍显示不出来。
2、修改代码,如加个console.log(1),导致代码热更新,浏览器菜单数据显示出来了。
3、刷新浏览器,菜单又不显示了,热更新代码会显示。
4、说明menuData数据获取到了,但是组件中的数据没有更新。
5、去看了官网,去看了ant design pro 的github中的bug,初步判断可以用menu属性的loading 来让layout重新渲染。
二、解决
src/layouts/BasicLayout.tsx中加入loading

const {
	loading,
} = props
	
return(
	<ProLayout
	 menu={{
        loading,
      }}
	...
	
	/>
)

...

export default connect(({ global, settings, menu }: ConnectState) => ({
  collapsed: global.collapsed,
  settings,
  menuData: menu.menuData,
  loading: menu.loading, // 引入loading
}))(BasicLayout);

src/models/menu.ts中新增loading的state

export interface MenuModelState {
  menuData: MenuDataItem[],
  loading: boolean,
}

const MenuModel: MenuModelType = {
  namespace: 'menu',
  state: {
    menuData: [],
    loading: true, // loading的初始值为true
  },

...

 reducers: {
    saveMenuData(state, action) {
      return {
        ...state,
        menuData: action.payload || [],
        loading: false, // 后台数据返回了,loading就改成false
      }
    }
  }

!!!注意,这样menu还未生效,需要删除config/defaultSetting.ts中的menu属性,不然会覆盖自己写的menu
config/defaultSetting.ts

const proSettings: DefaultSettings = {
  navTheme: 'dark',
  // 拂晓蓝:1890ff
  primaryColor: '#579c67',
  layout: 'side',
  contentWidth: 'Fluid',
  fixedHeader: false,
  fixSiderbar: true,
  colorWeak: false,
  // menu: { // 这个menu需要注释或删除,不然会覆盖自定义的menu
  //   locale: true,
  // },
  title: '正泰',
  pwa: false,
  iconfontUrl: '',
};

到这里,从服务器获取菜单就能正确显示了。

注意点

一、src/layouts/BasicLayout.tsx中的menu类型需要写明。
二、menu属性的loading值来强制菜单刷新。
三、menu属性会被defaultSetting覆盖。