动态路由实现分析
1. 概述
本项目采用了基于权限的动态路由方案,通过后端返回的权限数据,在前端动态生成路由配置并添加到路由实例中。这种方案实现了根据用户权限动态加载可访问的页面,提高了系统的安全性和灵活性。
2. 核心文件结构
3. 数据结构定义
3.1 权限数据结构
interface Permission {
code: string; // 权限编码
component: string | null; // 组件路径
created_at: string; // 创建时间
icon: string; // 菜单图标
id: number; // 权限ID
name: string; // 菜单名称
parent_id: number; // 父级菜单ID
path: string; // 路由路径
sort_order: number; // 排序顺序
status: number; // 状态(1为启用,0为禁用)
type: number; // 权限类型
}3.2 用户状态结构
interface UserState {
token: string; // 用户认证token
userInfo: UserInfo; // 用户信息
permissions: Permission[]; // 权限列表
isRoutesGenerated: boolean; // 路由是否已生成
}4. 动态路由生成机制
4.1 组件预加载
使用Vite的import.meta.globAPI预加载所有视图组件,生成组件路径到组件的映射:
const modules = import.meta.glob("@/views/**/*.vue");4.2 路由生成函数
export function generateRoutes(permissions: Permission[]): RouteRecordRaw[] {
// 1. 过滤有效的权限数据
const validPermissions = permissions.filter(
(p) => p.component && p.status === 1 && p.parent_id !== 0
);
// 2. 生成路由配置
const routes: RouteRecordRaw[] = validPermissions
.map((permission) => {
// 查找父级权限
const parentPermission = permissions.find(
(p) => p.id === permission.parent_id
);
// 转换组件路径并动态导入组件
const componentPath = permission.component!.replace("@/", "/src/");
const componentImport = modules[componentPath];
if (!componentImport) {
console.warn(`未找到对应的组件: ${permission.component}`);
return null;
}
// 生成路由对象
return {
path: `/${permission.path}`,
name: permission.code.replace(/:/g, ""),
component: componentImport,
meta: {
title: permission.name,
module: parentPermission?.name || "",
},
};
})
.filter(Boolean) as RouteRecordRaw[];
// 3. 构建完整路由配置
const newRoutes = [
{
path: "/",
name: "main",
component: () => import("@/views/Home.vue"),
children: routes, // 将生成的路由作为子路由添加到主路由
},
];
return newRoutes;
}5. 路由守卫实现
5.1 核心逻辑
router.beforeEach((to, _from, next) => {
// 初始化时从本地存储获取token和权限数据
userStore.initFromStorage();
const token = userStore.token;
const hasPermission = userStore.permissions.length > 0;
const isRoutesGenerated = userStore.isRoutesGenerated;
// 白名单路由直接放行
if (whiteList.includes(to.path)) {
// 已登录用户访问登录页,重定向到首页
if (token && to.path === "/login") {
next({ path: "/", replace: true });
} else {
next();
}
return;
}
// 未登录用户访问受限路由,跳转到登录页
if (!token) {
next({ path: "/login", replace: true });
return;
}
// 已登录但未生成动态路由且有权限数据
if (token && hasPermission && !isRoutesGenerated) {
try {
// 生成动态路由
const dynamicRoutes = generateRoutes(userStore.permissions);
// 添加路由到路由实例
dynamicRoutes.forEach((route) => {
router.addRoute(route);
});
// 更新路由生成状态
userStore.setRoutesGenerated(true);
// 设置默认重定向
if (dynamicRoutes[0]!.children!.length > 0 && to.path === "/") {
const firstRoute = dynamicRoutes[0]!.children![0];
if (firstRoute && firstRoute.path) {
next({ path: firstRoute.path, replace: true });
return;
}
}
// 重新导航以应用新路由
next({ ...to, replace: true });
} catch (error) {
console.error("Generate routes error:", error);
ElMessage.error("获取权限失败,请重新登录");
userStore.clearAuth();
next({ path: "/login", replace: true });
}
return;
}
// 已登录但没有权限数据
if (token && !hasPermission && !isRoutesGenerated) {
ElMessage.warning("暂无访问权限");
next(false); // 阻止导航
return;
}
// 已生成路由,直接放行
next();
});5.2 路由守卫流程图
开始导航
│
├─► 检查白名单
│ ├─► 是白名单路由
│ │ ├─► 已登录且访问登录页 → 重定向到首页
│ │ └─► 其他情况 → 放行
│ │
│ └─► 非白名单路由
│ ├─► 未登录 → 重定向到登录页
│ │
│ └─► 已登录
│ ├─► 已生成路由 → 放行
│ │
│ └─► 未生成路由
│ ├─► 没有权限数据 → 提示无权限,阻止导航
│ │
│ └─► 有权限数据
│ ├─► 生成动态路由
│ ├─► 添加路由到路由实例
│ ├─► 更新路由生成状态
│ ├─► 当前是首页 → 重定向到第一个有权限的页面
│ └─► 其他页面 → 重新导航以应用新路由
│
└─► 结束6. 用户状态管理
6.1 状态初始化
initFromStorage() {
// 从localStorage或sessionStorage获取token
const localToken = localStorage.getItem("token");
const sessionToken = sessionStorage.getItem("token");
this.token = localToken || sessionToken || "";
// 从localStorage恢复用户信息
const storedUserInfo = localStorage.getItem("userInfo");
if (storedUserInfo) {
try {
this.userInfo = JSON.parse(storedUserInfo);
} catch (e) {
console.error("Failed to parse stored user info", e);
}
}
// 从localStorage恢复权限数据
const storedPermissions = localStorage.getItem("permissions");
if (storedPermissions) {
try {
this.permissions = JSON.parse(storedPermissions);
} catch (e) {
console.error("Failed to parse stored permissions", e);
}
}
}6.2 权限数据更新
setPermissions(permissions: Permission[]) {
this.permissions = permissions;
this.isRoutesGenerated = false; // 重置路由生成状态
// 存储权限数据到localStorage
localStorage.setItem("permissions", JSON.stringify(permissions));
}6.3 认证清除
clearAuth() {
this.token = "";
this.userInfo = {};
this.permissions = [];
this.isRoutesGenerated = false;
localStorage.removeItem("token");
localStorage.removeItem("userInfo");
localStorage.removeItem("permissions");
sessionStorage.removeItem("token");
}7. 动态路由工作流程
7.1 用户登录流程
用户输入用户名和密码登录
登录成功后,后端返回token和权限数据
前端调用
setAuth存储token,调用setPermissions存储权限数据用户访问受限路由,触发路由守卫
路由守卫检测到已登录但未生成路由,调用
generateRoutes生成动态路由将生成的路由添加到路由实例中,更新路由生成状态
完成导航,显示用户可访问的页面
7.2 页面刷新流程
页面刷新,应用重新初始化
路由守卫触发,调用
initFromStorage从本地存储恢复状态如果本地存储有token和权限数据,调用
generateRoutes生成动态路由添加路由到路由实例,完成导航
8. 代码优化建议
8.1 组件路径校验增强
当前代码中,当组件路径无效时,只会输出警告并跳过该路由。建议添加更严格的校验和错误处理:
if (!componentImport) {
console.error(`组件路径无效: ${permission.component}`);
// 可以选择抛出错误或记录到错误监控系统
return null;
}8.2 路由缓存机制
对于频繁访问的页面,可以考虑实现路由缓存机制,提高页面切换性能:
// 在路由配置中添加缓存标记
return {
path: `/${permission.path}`,
name: permission.code.replace(/:/g, ""),
component: componentImport,
meta: {
title: permission.name,
module: parentPermission?.name || "",
keepAlive: true, // 添加缓存标记
},
};8.3 权限粒度细化
当前实现只支持页面级权限,建议扩展支持按钮级权限:
// 在Permission接口中添加按钮权限字段
export interface Permission {
// ... 现有字段
buttonPermissions?: string[]; // 按钮权限列表
}
// 在组件中使用权限判断
const hasPermission = (permissionCode: string) => {
return userStore.permissions.some((p) =>
p.buttonPermissions?.includes(permissionCode)
);
};8.4 路由生成错误处理增强
当前代码中,路由生成失败时会清除认证信息并重定向到登录页,建议添加更详细的错误日志和恢复机制:
try {
// 生成动态路由
const dynamicRoutes = generateRoutes(userStore.permissions);
// ...
} catch (error) {
console.error("Generate routes error:", error);
// 记录错误到监控系统
// 可以考虑保留当前认证状态,仅提示错误
ElMessage.error("路由生成失败,请刷新页面重试");
next(false);
}8.5 路由懒加载优化
结合Vue 3的Suspense组件,优化路由切换体验:
<!-- App.vue -->
<template>
<Suspense>
<template #default>
<router-view />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</template>9. 总结
本项目的动态路由实现具有以下特点:
基于权限的动态生成:根据后端返回的权限数据,动态生成用户可访问的路由
组件按需加载:使用Vite的
import.meta.glob实现组件的动态导入状态持久化:将token、用户信息和权限数据存储到本地,页面刷新后自动恢复
完善的路由守卫:处理登录验证、动态路由生成和导航控制
灵活的权限管理:支持多级菜单和权限控制
这种动态路由方案提高了系统的安全性和灵活性,便于后端统一管理用户权限和菜单配置,同时减轻了前端的维护成本。