原文见:https://www.yuque.com/asuncat/htn2dd/wgnx0u
版本信息
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覆盖。