React Router 附属功能
本文介绍 React Router 的扩展功能,包括权限控制、菜单系统、页签管理、KeepAlive 缓存等进行说明,如需进行代码对照,可参考目录下 react19_ts 项目内实际代码,文档与代码会在git上持续补充
git仓库地址:https://gitee.com/xiaoli-account/react19_ts> 本文档介绍 React Router 的扩展功能,包括权限控制、菜单系统、页签管理、KeepAlive 缓存等。这些功能建立在前端路由核心能力之上,属于项目框架的增强特性。
目录
.├── 1. 权限系统│ ├── 1.1 三层权限模型│ ├── 1.2 权限初始化│ ├── 1.3 路由过滤│ ├── 1.4 按钮权限判断│ └── 1.5 API 权限装饰器├── 2. 菜单系统├── 3. 面包屑与页签 Tabs│ ├── 3.1 面包屑│ └── 3.2 页签 Tabs├── 4. KeepAlive 缓存策略├── 5. 路由守卫 & 进度条├── 6. 状态管理└── 7. 关键文件索引
1. 权限系统
权限控制文件:
`src/layout/utils/leePermission.ts`
有关权限系统的设计,请查看
1.1 三层权限模型
| 层级 | 权限标识格式 | 控制粒度 | 开关变量 ||------|------------|---------|---------|| 页面级 | `page:module:action` | 路由能否访问 | `routePermEnabled` || 按钮级 | `btn:module:action` | 按钮/操作权限 | `btnPermEnabled` || API 接口级 | 接口路径 `/api/user/list` | 接口调用权限 | `apiPermEnabled` |
1.2 权限初始化
在用户登录成功后调用 `initLeePermission()`,传入后端返回的权限列表:
import { initLeePermission } from "@/layout/utils/leePermission";initLeePermission({ pagePermissionList: userInfo.pages, // 页面权限列表 btnPermissionList: userInfo.btns, // 按钮权限列表 apiPermissionList: userInfo.apis, // API 权限列表 btnPermEnabled: true, // 开启按钮权限校验 routePermEnabled: true, // 开启页面权限校验 type: "layout", // layout=初次加载 / refresh=刷新恢复});
性能优化:当权限列表超过 100 条时,自动切换为 `Map` 查询(O(1)),避免大数组 `includes` 的性能问题。
1.3 路由过滤
import { filterRoutesByPerm } from "@/layout/utils/leePermission";// 模式一:前端控制(权限标识过滤前端全量路由)const authorizedRoutes = filterRoutesByPerm(allRoutes);// 模式二:后端控制(前端静态 + 后端动态路由合并)const authorizedRoutes = filterRoutesByPerm(staticWebRoutes, asyncServerRoutes);
过滤逻辑:递归遍历路由树,通过 `hasRoutePermission(meta.pagePermission)` 判断每个节点的访问权限。
1.4 按钮权限判断
import { hasBtnPermission, hasAnyBtnPermission, hasAllBtnPermission } from "@/layout/utils/leePermission";// 单一权限判断{hasBtnPermission("btn:system:user:add") && <Button>新增用户</Button>}// 满足任意一个权限{hasAnyBtnPermission(["btn:system:user:edit", "btn:system:user:admin"]) && <Button>编辑</Button>}// 满足所有权限{hasAllBtnPermission(["btn:system:user:delete", "btn:system:admin"]) && <Button>批量删除</Button>}
1.5 API 权限装饰器
import { LeeApiPermission } from "@/layout/utils/leePermission";classUserService{ @LeeApiPermission("/user/deleteUserCascade") deleteUser(userId: string) { return $post("/user/deleteUserCascade", { userId }); }}
⚠️ 注意⚠️ `@LeeApiPermission` 必须放在方法最外层装饰器位置,否则接口可能在权限校验前就已调用。
2. 菜单系统
菜单 Hook:`src/layout/hooks/use-menu.ts`
`useMenu` Hook 将路由配置自动转换为 Antd Menu 所需的数据格式:
const { menus, getSelectedKeys, getOpenKeys, getBreadcrumbItems, syncMenuState } = useMenu();
核心能力:
| 方法 | 作用 ||------|------|| `menus` | 已转换的 Antd 菜单数据(自动过滤 `hidden: true`) || `getSelectedKeys(pathname)` | 根据当前 URL 获取高亮菜单项(支持前缀匹配) || `getOpenKeys(pathname)` | 根据当前 URL 获取展开的父级菜单 || `getBreadcrumbItems(pathname)` | 根据当前 URL 生成面包屑数据 || `getBreadcrumbItem(pathname)` | 获取当前页面标题 || `syncMenuState(pathname)` | 同步菜单高亮/展开状态到缓存 || `findMenuItem(path)` | 根据路径查找菜单项 |
菜单渲染流程:
routes.ts configRoutes → useMenu().getMenusData() → 找到 path="/" 且有 children 的 layoutRoute → 递归 routeToMenuItem() → 跳过 hidden: true → 拼接完整路径 → 解析 icon(动态获取 @ant-design/icons) → 处理外链(external + target) → 递归子菜单 → 输出 Antd MenuItemType[]
3. 面包屑与页签 Tabs
3.1 面包屑
面包屑通过
`useMenu().getBreadcrumbItems(pathname)` 生成,支持:
- 🏠 固定首项 Home 图标链接
- 多层嵌套路由的路径回溯
- 国际化 `t(meta.i18n)` 翻译
3.2 页签 Tabs
页签系统由 `src/layout/stores/tabs-store.ts` + Tab 组件实现:
interface TagView { path: string; // 路由路径 title: string; // 显示标题 name?: string; // 路由名称 keepAlive?: boolean; // 是否缓存 locale?: string; // i18n key}
支持的操作:
| 操作 | 说明 ||------|------|| `addView(view)` | 添加页签(自动去重,超过 `maxTabCount=10` 自动踢出最早的) || `delView(view)` | 关闭页签(自动跳转相邻页签) || `delOthersViews(view)` | 关闭其他页签 || `delAllViews()` | 关闭全部页签 |
4. KeepAlive 缓存策略
React Router 本身不支持 KeepAlive,本项目通过 Tab 组件 + useRef Map + CSS 显隐 实现:
┌─────────────────────────────────────┐│ componentsRef = new Map() ││ key: pathname → value: outlet │└─────────────────────────────────────┘用户访问 /dashboard (keepAlive: true) → componentsRef.set("/dashboard", outlet) → Tab 渲染 outlet用户切换到 /user-management → /dashboard 的 outlet 依然存在于 Map 中 → Antd Tabs 通过 display:none 隐藏 /dashboard 的 DOM → 组件 State 保留,不触发 unmount用户切回 /dashboard → 从 componentsRef.get("/dashboard") 取出缓存 → 恢复之前的滚动位置和表单状态
核心代码位于 `src/layout/lee-basic-layout/tab/index.tsx`,策略如下:
| 场景 | 渲染策略 ||------|---------|| 当前激活 Tab | 直接渲染 `outlet`(React Router 最新组件) || 非激活 + `keepAlive: true` | 从 `componentsRef` 取缓存(不卸载,CSS 隐藏) || 非激活 + `keepAlive: false` | 渲染 `null`(卸载释放内存) |
5. 路由守卫 & 进度条
本项目通过 `RouteProgress` 组件实现路由切换时的顶部进度条动画:
// src/layout/router/index.tsxfunction RouteProgress() { const location = useLocation(); useEffect(() => { startProgress(); // 路由变化 → 开始加载 const timer = setTimeout(() => { doneProgress(); // 300ms 后完成 }, 300); return () => { clearTimeout(timer); doneProgress(); // 清理 }; }, [location.pathname]); return null;}
底层基于 `nprogress` 库
(`src/layout/utils/nprogress.ts`),配置如下:
NProgress.configure({ showSpinner: false, // 不显示右上角旋转图标 trickleSpeed: 200, // 自动递增间隔 ms minimum: 0.3, // 初始最小百分比 easing: "ease", // 动画缓动函数 speed: 500, // 递增速度 ms});
自定义样式文件:`src/styles/nprogress.scss`
6. 状态管理
路由系统的状态管理基于 Zustand:
| Store | 文件 | 持久化 | 说明 ||-------|------|--------|------|| `useRouterStore` | `stores/router-store.ts` | ❌ | 路由配置。**禁止持久化**,否则 React 组件引用丢失 || `useMenuStore` | `stores/menu-store.ts` | ✅ localStorage | 菜单展开/选中状态、路由菜单数据 || `useTabsStore` | `stores/tabs-store.ts` | ✅ localStorage | 已打开页签、缓存页面列表 |
⚠️ 关键设计决策:`useRouterStore.routes` 不做持久化。因为 `RouteItem.component` 包含函数引用(`() => import(...)`),序列化后无法恢复。刷新时通过 `menuStore` 的持久化数据 + 权限重新初始化来恢复路由配置。
7. 关键文件索引
| 文件 | 职责 ||------|------|| `src/layout/utils/nprogress.ts` | 路由进度条 || `src/layout/utils/leePermission.ts` | 三层权限系统 || `src/layout/stores/router-store.ts` | 路由状态 Store || `src/layout/stores/menu-store.ts` | 菜单状态 Store(持久化) || `src/layout/stores/tabs-store.ts` | 页签状态 Store(持久化) || `src/layout/hooks/use-menu.ts` | 菜单数据转换 Hook || `src/layout/lee-basic-layout/tab/index.tsx` | Tab 页签 + KeepAlive 实现 || `src/layout/lee-basic-layout/breadcrumb/index.tsx` | 面包屑组件 |