在中后台前端开发中,动态路由权限控制是核心能力之一 ------ 系统需要根据当前登录用户的角色 / 权限,动态展示可访问的菜单并拦截无权限的路由访问。本文将基于 Umi 框架(Ant Design Pro 底层框架),详解如何实现精细化的动态路由权限判断。
一、核心场景与技术背景
1. 业务场景
- 不同角色(如管理员、普通运营、访客)看到不同的菜单列表
- 禁止用户通过手动输入 URL 访问无权限的路由
- 兼容后端返回的多格式菜单数据(如 name/title 字段混用)
2. 技术栈基础
- 框架:Umi 4(Ant Design Pro v6)
- 核心能力:Umi 的 access 权限控制体系
- 存储:sessionStorage 存储用户菜单数据
- 数据结构:TypeScript 接口约束菜单 / 路由数据
二、完整实现代码与解析
1. 类型定义(核心接口)
首先通过 TypeScript 接口约束菜单、路由、用户数据结构,保证类型安全:
typescript
运行
// src/access.d.ts
// 菜单单项接口(兼容后端多字段返回)
interface MenuItem {
path?: string; // 菜单路径
name?: string; // 菜单名称(优先)
title?: string; // 菜单标题(兼容字段)
children?: MenuItem[]; // 子菜单
[key: string]: any; // 兼容其他自定义字段
}
// 本地存储的用户菜单数据结构
interface UserMenuData {
menu?: MenuItem[]; // 用户拥有的菜单列表
}
// 路由匹配参数接口(Umi 路由对象子集)
interface Route {
path?: string; // 路由路径
name?: string; // 路由名称
}
// 扩展 Umi 的 Access 类型(可选)
declare module 'umi' {
interface AccessState {
currentUser?: API.CurrentUser;
}
}
2. Access 权限控制核心逻辑
Umi 的 access 函数是权限控制的核心,返回的权限方法可在全局使用:
typescript
运行
// src/access.ts
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
// 从初始状态解构当前用户(Umi 初始化时传入)
const { currentUser } = initialState ?? {};
// 存储用户菜单数据的变量
let menuData: MenuItem[] = [];
return {
// 核心方法:判断路由是否有权限访问
hasRoute: (route: Route): boolean => {
try {
// 1. 从 sessionStorage 读取用户菜单数据
const userDataStr = sessionStorage.getItem('userinfo');
if (userDataStr) {
const userData: UserMenuData = JSON.parse(userDataStr);
menuData = userData.menu || [];
}
} catch (error) {
// 异常处理:解析失败时重置菜单数据
console.error('解析用户菜单数据失败:', error);
menuData = [];
}
// 2. 根路径特殊处理:放行重定向逻辑
if (route.path === '/') {
return false; // 让路由配置中的 redirect 生效
}
// 3. 递归遍历菜单树,匹配路由
const matchMenu = (menuList: MenuItem[]): boolean => {
return menuList.some(item => {
// 优先使用 name,兼容 title 字段
const menuName = item.name || item.title;
const routeName = route.name;
// 路径匹配逻辑(含特殊场景兼容)
let matchPath = false;
if (item.path && route.path) {
// 兼容:菜单根路径 "/" 匹配路由 "/index"
if (item.path === '/' && route.path === '/index') {
matchPath = true;
} else {
// 常规路径精确匹配
matchPath = item.path === route.path;
}
}
// 名称匹配逻辑
const matchName = menuName === routeName;
// 路径或名称匹配则返回有权限
if (matchPath || matchName) {
return true;
}
// 递归检查子菜单
if (item.children && item.children.length > 0) {
return matchMenu(item.children);
}
// 无匹配且无子菜单
return false;
});
};
// 4. 执行匹配并返回结果
return matchMenu(menuData);
},
};
}
3. 关键逻辑解析
(1)菜单数据加载
- 从
sessionStorage.getItem('userinfo')读取用户菜单数据(登录后由接口写入) - 异常捕获:防止 JSON 解析失败导致权限逻辑崩溃
- 兜底处理:解析失败时重置
menuData为空数组
(2)根路径特殊处理
- 路由配置中根路径
'/'通常配置为重定向(如跳转到/dashboard) - 返回
false让 Umi 的路由重定向逻辑生效,避免权限拦截影响默认跳转
(3)递归菜单匹配
- 双重匹配规则:路径(path)匹配 + 名称(name/title)匹配,兼容不同后端返回格式
- 递归遍历 :支持多级嵌套菜单(如
/system/user→/system/role) - 短路逻辑 :
Array.some()找到匹配项立即返回,提升性能
三、实际应用场景
1. 路由守卫(菜单过滤)
在 config/routes.ts 中使用 access 控制路由是否显示:
typescript
运行
// config/routes.ts
export default [
{
path: '/dashboard',
name: 'dashboard',
component: './Dashboard',
access: 'hasRoute', // 使用 access 中的 hasRoute 方法判断权限
},
{
path: '/system/user',
name: 'systemUser',
component: './System/User',
access: 'hasRoute',
},
];
2. 菜单渲染控制
在菜单组件中使用 useAccess 钩子过滤无权限菜单:
typescript
运行
// src/components/Menu/index.tsx
import { useAccess } from 'umi';
const CustomMenu = () => {
const access = useAccess();
const menuData = useSelector(state => state.menu.data);
// 过滤无权限的菜单
const filteredMenu = menuData.filter(item => {
return access.hasRoute({ path: item.path, name: item.name });
});
return <Menu data={filteredMenu} />;
};
3. 手动跳转拦截
在页面跳转前校验权限,防止无权限访问:
typescript
运行
// src/utils/permission.ts
import { useAccess, history } from 'umi';
export const checkRouteAccess = (path: string, name?: string) => {
const access = useAccess();
const hasPermission = access.hasRoute({ path, name });
if (!hasPermission) {
history.push('/403'); // 跳转到无权限页面
return false;
}
return true;
};
四、扩展优化方案
1. 性能优化
-
缓存匹配结果 :对已匹配的路由结果做缓存,避免重复递归遍历
typescript
运行
// 新增缓存对象 const routeCache = new Map<string, boolean>(); const matchMenu = (menuList: MenuItem[]): boolean => { const cacheKey = `${route.path}-${route.name}`; if (routeCache.has(cacheKey)) { return routeCache.get(cacheKey)!; } // 原有匹配逻辑... const result = menuList.some(/** 匹配逻辑 **/); routeCache.set(cacheKey, result); return result; };
2. 支持模糊匹配
适配动态路由(如 /detail/:id),扩展路径匹配逻辑:
typescript
运行
// 模糊路径匹配(适配动态路由)
const matchDynamicPath = (menuPath: string, routePath: string): boolean => {
// 移除动态参数(如 /detail/:id → /detail)
const menuPathPure = menuPath.replace(/:\w+/g, '');
const routePathPure = routePath.replace(/:\w+/g, '');
return menuPathPure === routePathPure;
};
3. 权限刷新机制
用户切换角色 / 权限后,清空缓存并重新加载菜单:
typescript
运行
// 新增刷新权限方法
refreshAccess: () => {
menuData = [];
routeCache.clear(); // 清空缓存
const userDataStr = sessionStorage.getItem('userinfo');
if (userDataStr) {
const userData: UserMenuData = JSON.parse(userDataStr);
menuData = userData.menu || [];
}
}
五、总结
本文基于 Umi 框架的 access 权限体系,实现了一套通用的动态路由权限控制方案,核心亮点:
- 类型安全:通过 TypeScript 接口约束菜单 / 路由数据结构
- 兼容灵活:支持 name/title 字段混用、根路径特殊匹配
- 性能可靠:递归遍历 + 异常捕获 + 可选缓存优化
- 全局可用:权限方法可在路由配置、组件、工具函数中复用
该方案可直接应用于 Ant Design Pro 项目,也可适配其他基于 Umi 的中后台系统,只需根据实际业务调整菜单匹配规则(如模糊匹配、权限码匹配等)即可。