持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情
上节课中我们完成了页面的大致布局的编写,今天我们主要把重点放在菜单配置中,为什么这么一个简单的菜单配置要单独写一篇文章来说明呢?
因为他在后续的“动态菜单”,权限校验等环节都有很重要的作用。
首先你要先把菜单数据上升到页面级数据,虽然它只是一个组件,但是它里面的数据需要和页面数据(主要是路由)关联上,几乎所有的导航组件,都需要有这一点的意识。
首先我们需要获取到当前项目的所有路由信息,这有两种方式,一种是配置式,自己整理出一个清单,每增加一个页面都更新这个清单,有个好处就是后续菜单交由服务端管控的时候,可以直接将这份数据给他。坏处就是页面数是固定的,后续想做动态菜单,有点困难,因为你配置的路由信息需要是一个“最大值”,否则未配置的页面没有被引用则不会被编译。
另一种就是约定式的,新建一个页面,即增加一个菜单信息,我们通过 Umi 提供的 API 获取到最新的页面路由信息,调用一些工具类,将他们转换成菜单数据,后面维护心智很低。约定式的方式,所有的页面都会被构建,只是通过菜单加权限来控制页面是否可访问,可以实现类似动态路由这样的需求。缺点就是需要独立维护一份菜单数据,主要是页面名称的“翻译文档“。比如首页 ”/home“ 在菜单中应该显示 “首页”。
获取当前页面数据
Umi@4 中要获取页面配置非常的简单,只需要使用 useAppData
即可,它返回全局的应用数据。
declare function useAppData(): {
routes: Record<id, Route>;
routeComponents: Record<id, Promise<React.ReactComponent>>;
clientRoutes: ClientRoute[];
pluginManager: any;
rootElement: string;
basename: string;
clientLoaderData: { [routeKey: string]: any };
preloadRoute: (to: string) => void;
};
复制代码
routes
和 clientRoutes
这两个数据都是路由数据,前者是对象,以 pathname
为 key
,以 parentId
来标记层级和嵌套关系。后者是一个数组,以 children
来表示树形结构。
const routes = {
'a':{
parentId: "b"
path: "a"
},
'b':{
path: "b"
},
}
复制代码
const clientRoutes = [{
path: "b",
children:[{
path: "a"
}]
}]
复制代码
以上两个数据“对等”。
所以我们要取到当前的所有的路由配置信息,则
import { useAppData } from "umi";
const App = ()=>{
const { clientRoutes } = useAppData();
const { children } = clientRoutes[0];
}
复制代码
将路由转化成菜单数据
const clientRoutes = [{
path: "b",
children:[{
path: "a"
}]
}];
// 转化为
const menuData = [{
key:"/b",
icon:<PieChartOutlined />,
label:"首页",
children:[{
key:"/a",
icon:<UserOutlined />,
label:"用户",
}]
}];
复制代码
通过观察分析,我们发现,其实路由数据中,我们只有 path
和 children
数据有用,而菜单数据中,我们还需要 icon
和 label
,这时候就需要引入我们前面提到的 翻译文档
了。
const menuHash: any = {
"/": {
label: "首页",
icon: <PieChartOutlined />,
},
user: {
label: "用户",
icon: <UserOutlined />,
},
};
复制代码
至此我们的 路由转菜单的工具类
为:
const getItem = (path: string, children?: MenuItem[]) => {
const route = menuHash[path];
return {
key: path.startsWith("/") ? path : `/${path}`,
icon: route?.icon || <></>,
children,
label: route?.label || path,
} as MenuItem;
};
const routesToMenu = (routes: any[]): MenuItem[] => {
return routes
.map((route) => {
const { path, children } = route;
if (children) {
return getItem(path, routesToMenu(children));
}
return getItem(path);
});
};
复制代码
运行项目,访问 http://127.0.0.1:8888/
这是你会发现,菜单中有很多我们之前写的 demo 页面,我们并不想让他们展示出来。所以我们需要增加一个访问权限的黑名单。
const unaccessible = ["/hooks", "/useEffect", "/usemodel", "/useState"];
复制代码
只要简单的修改一下,我们的 routesToMenu
方法即可。
const routesToMenu = (routes: any[]): MenuItem[] => {
return routes
.filter((i) => {
const path = i.path.startsWith("/") ? i.path : `/${i.path}`;
return !unaccessible.includes(path);
})
.map((route) => {
const { path, children } = route;
if (children) {
return getItem(path, routesToMenu(children));
}
return getItem(path);
});
};
复制代码
保存代码,你讲看到菜单中只有两个数据了。
增加页面权限
但是这只是将路由入口隐藏了,如果用户知道你的路由信息,比如此时我们直接当问 http://127.0.0.1:8888/usemodel
,虽然菜单已经过滤了但是我们依旧可以直达页面。
其实原理也很简单,只要判断当前页面 pathname 在我们的不可访问清单就返回 403 页面即可,这个要看你们项目中的权限采用的是黑名单模式还是白名单模式了,黑名单模式匹配上拦截,白名单模式匹配上放行。
import { Result, Button } from "antd";
if (unaccessible.includes(location.pathname)) {
return (
<Result
status="403"
title="403"
subTitle="抱歉,你没有权限访问这个页面!"
extra={
<Button type="primary" onClick={() => navigate(-1)}>
返回上一个页面
</Button>
}
/>
);
}
复制代码
菜单与路由跳转
需要实现的功能,点击菜单触发路由跳转,当前页面对应的菜单项需要高亮显示。
import { useAppData, useNavigate, useLocation } from "umi";
const App = ()=>{
const navigate = useNavigate();
const location = useLocation();
const { clientRoutes } = useAppData();
const { children } = clientRoutes[0];
const items = routesToMenu(children);
return <Menu
theme="dark"
onClick={(e) => {
navigate(e?.key);
}}
defaultSelectedKeys={[location.pathname]}
mode="inline"
items={items}
/>;
}
复制代码
至此,我们的菜单与权限部分的所有功能都开发完毕。这里面需要引申到项目的权限管控上,将 unaccessible
和用户的登录信息关联上,就可以了。如果有面试官问你,你们项目中的权限部分是怎么做的?如果用户知道你的页面url是否可以直接访问页面,如何拦截?你应该可以回答的很明白了。当然这节课只是为了讲明白原理,在实际项目开发中我们可以用上 layout 和 access 插件的组合来更合理的完成权限和菜单。