前言
想象一下,如果你的应用能像超级英雄一样,识别谁是“好人”,谁是“坏人”,并根据他们的身份决定他们能看到什么内容,那将是多么酷炫的体验!在这篇文章中,我们将一起探索如何利用 Umi V4 的强大功能和 Ant Design Pro 的优雅设计,构建一个既安全又灵活的权限管理系统。
代码实战
初始化:Umi V4贴心的集成了@ant-design/pro-components/pro-layout,只需开启配置中的layout选项即可。快速搭建后台架构,初始化项目选择Ant Design Pro为模板
npx create-umi@latest
mock后端数据:定义后端返回数据结构,直接使用umi提供的mock功能。格式如下:
mock/route.ts
export default {
'GET /api/v1/routes': (req, res) => {
res.json({
success: true,
data: [
{
menuId: 'home',
parentId: null,
enable: true,
name: '首页',
sort: 1000,
path: '/home',
direct: true,
},
{
menuId: 'access_management',
parentId: null,
enable: true,
name: '权限演示',
sort: 2000,
path: '/access',
direct: false,
},
{
menuId: 'user_management',
parentId: 'access_management',
enable: true,
name: '用户',
sort: 2001,
path: '/access/user',
direct: false,
},
{
menuId: 'role_management',
parentId: 'access_management',
enable: true,
name: '管理员',
sort: 2002,
path: '/access/role',
direct: false,
// 测试权限功能
access: 'canSeeAdmin',
},
],
errorCode: 0,
});
},
};
解析路由数据:首先梳理下执行流程,用户登录→路由解析→跳转首页。这里存在一个问题,页面刷新后文件执行顺序是:global.(t/j)sx→app.(t/j)sx/parseRoutes,但跳转首页是路由跳转,页面并不会刷新,所以需要在login页面手动触发页面刷新并且需要判断登录态跳转,再依次执行组装路由跳转等
当然如果你获取路由数据的接口不需要鉴权,那你完全不用在global中执行,可以直接在parseRoutes方法中进行请求,相对应login页面也不需刷新页面,直接跳转即可
global.(t/j)sx
try {
// 判断是否登录
if (localStorage.getItem(TOKEN) !== null) {
const { data: routesData } = await fetch('/api/v1/routes', {
method: 'GET',
headers: { 'Content-Type': 'application/json',Authorization: localStorage.getItem(TOKEN) },
}).then((res) => res.json());
if (routesData) {
window.dynamic_routes = routesData;
}
}
} catch (err) {
console.error(err);
}
export {};
app.(t/j)sx
export function patchRoutes({ routes, routeComponents }) {
if (window.dynamic_routes) {
const currentRouteIndex = Object.keys(routes).length;
const parsedRoutes = parseRoutes(
window.dynamic_routes,
currentRouteIndex,
);
Object.assign(routes, parsedRoutes.routes);
Object.assign(routeComponents, parsedRoutes.routeComponents);
}
}
utils/route.(t/j)sx
function parseRoutes(routesRaw, beginIdx: number) {
const routes = {};
const routeComponents = {};
const routeParentMap = new Map<string, number>();
let currentIdx = beginIdx;
routesRaw.forEach((route) => {
let effectiveRoute = true;
const formattedRoutePath = route.path;
const routePath = generateRoutePath(formattedRoutePath);
const componentPath = generateComponentPath(formattedRoutePath);
const filePath = generateFilePath(formattedRoutePath);
if (route.direct) {
const tempRoute = {
id: currentIdx.toString(),
parentId: 'ant-design-pro-layout',
name: route.name,
path: routePath,
file: filePath,
access: route.access,
};
routes[currentIdx] = tempRoute;
const tempComponent = lazy(() => import(`@/pages/${componentPath}`));
routeComponents[currentIdx] = tempComponent;
} else {
// 判断是否非一级路由
if (!route.parentId) {
// 正在处理的为一级路由
const tempRoute = {
id: currentIdx.toString(),
parentId: 'ant-design-pro-layout',
name: route.name,
path: routePath,
access: route.access,
};
routes[currentIdx] = tempRoute;
const tempComponent = Outlet;
routeComponents[currentIdx] = tempComponent;
routeParentMap.set(route.menuId, currentIdx);
} else {
// 非一级路由
// 获取父级路由ID
const realParentId = routeParentMap.get(route.parentId);
if (realParentId) {
const tempRoute = {
id: currentIdx.toString(),
parentId: realParentId.toString(),
path: routePath,
access: route.access,
...(route.redirect
? { redirect: route.redirectUrl }
: { name: route.name, file: filePath }),
};
routes[currentIdx] = tempRoute;
const tempComponent = route.redirect
? Outlet
: lazy(() => import(`@/pages/${componentPath}`));
routeComponents[currentIdx] = tempComponent;
} else {
// 找不到父级路由,路由无效,workingIdx不自增
effectiveRoute = false;
}
}
}
if (effectiveRoute) {
// 当路由有效时,将workingIdx加一
currentIdx += 1;
}
});
return {
routes,
routeComponents,
};
}
login.(t/j)sx
const onFinish = async (values) => {
try {
if (values) {
const msg = await login(values);
if (msg?.token) {
localStorage.setItem(TOKEN, msg.token);
messageApi.success('登录成功!');
window.location.reload();
return;
}
messageApi.error(msg?.msg || '登录失败,请重试!');
}
} catch (error) {
messageApi.error('登录失败,请重试!');
}
};
if (localStorage.getItem(TOKEN) !== null) {
return <Navigate to="/" replace />;
}
关于权限
已采用后端路由方式,权限问题其实已经不需要前端来做什么,后端只需根据登录用户权限返回对应的路由即可。但前端也可以实现,直接使用Umi提供的access方案,不仅可以实现权限路由,甚至到按钮级别。直接调整接口access字段及以下文件测试即可,parseRoutes已具备功能。
src/access.ts
export default (initialState) => {
// 此处数据由登录时存入
const { userId, role } = initialState;
return {
canSeeAdmin: role === 'admin',
};
};
想实现按钮级权限,直接根据文档调用useAccess即可
import { PageContainer } from '@ant-design/pro-components';
import { Access, useAccess } from '@umijs/max';
import { Button } from 'antd';
const AccessPage: React.FC = () => {
const access = useAccess();
return (
<PageContainer
ghost
header={{
title: '权限示例',
}}
>
<Access accessible={access.canSeeAdmin}>
<Button>只有 Admin 可以看到这个按钮</Button>
</Access>
</PageContainer>
);
};
export default AccessPage;
最后,如果你想要hash路由,更改config.(t/j)s文件中history选项即可
history: { type: 'hash' }
存在问题
1.global执行过早,umi插件未初始化。导致request配置获取不到,本地开发受限制会产生跨域。但本地开发可以启用umi的mock功能
注:标题和前言是cursor生成的,后续有内容再加