实现页面级权限控制
方案:后端返回完整路由结构,前端动态组件映射
1. 设计思路
- 初始状态: 路由表中只有“登录页”、“404”等基础静态路由页面。
- 登录: 用户登录,获取 Token。
- 获取权限: 前端拿着 Token 请求后端接口,后端返回该用户的菜单树(路由表)。
- 动态挂载: 前端将后端返回的 JSON 树转换为 Vue Router 路由对象,使用
router.addRoute()挂载。 - 渲染菜单: 侧边栏菜单基于“静态路由 + 动态路由”的合并结果进行渲染。
2. 后端数据结构约定
后端返回的数据结构通常如下。前端组件通常是以字符串形式存储在数据库中的(如 "views/user/index"),前端需要负责将其转换为真正的组件引用。
{
"code": 200,
"data": [
{
"name": "System",
"path": "/system",
"component": "Layout",
"meta": { "title": "系统管理" },
"children": [
{
"name": "UserManage",
"path": "user",
"component": "views/system/user/index",
"meta": { "title": "用户管理", "roles": ["admin"] }
}
]
}
]
}
3. 前端实现步骤
第一步:Vite 组件自动导入
在 Vite 中,使用import.meta.glob 是实现动态路由,在 src/store/permission.js 中预先读取所有 Views 文件:
// 匹配 views 下所有的 .vue 文件
const modules = import.meta.glob('../views/**/*.vue')
第二步:Pinia 状态管理
我们需要一个 Store 来处理路由数据的获取和格式化。
关键逻辑: 将后端返回的字符串 component 映射为前端的组件对象。
// src/store/permission.js
import { defineStore } from 'pinia'
import { constantRoutes } from '@/router' // 引入静态路由
import { getUserNav } from '@/api/user'
import Layout from '@/layout/index.vue'
const modules = import.meta.glob('../views/**/*.vue')
export const usePermissionStore = defineStore('permission', {
state: () => ({
routes: [] // 最终用于渲染侧边栏的完整菜单
}),
actions: {
generateRoutes() {
return new Promise(async (resolve) => {
const res = await getUserNav()
const remoteRoutes = res.data
// 1. 递归转换后端路由
const accessedRoutes = this.filterAsyncRoutes(remoteRoutes)
// 3. 合并菜单:静态 + 动态
this.routes = constantRoutes.concat(accessedRoutes)
resolve(accessedRoutes)
})
},
filterAsyncRoutes(routes) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
// 处理 Layout 组件
if (tmp.component === 'Layout') {
tmp.component = Layout
} else {
// 处理业务组件
const componentPath = `../${tmp.component}.vue`
if (modules[componentPath]) {
tmp.component = modules[componentPath]
} else {
console.error('组件路径错误:', componentPath)
}
}
if (tmp.children) {
tmp.children = this.filterAsyncRoutes(tmp.children)
}
res.push(tmp)
})
return res
}
}
})
第三步:路由守卫 (Gatekeeper)
在 src/permission.js 中,我们利用 router.beforeEach 来拦截请求。
import router from './router'
import { usePermissionStore } from '@/store/permission'
let hasRoutes = false // 标记是否已加载
router.beforeEach(async (to, from, next) => {
const permissionStore = usePermissionStore()
const hasToken = localStorage.getItem('token') // Token 检查
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
} else {
if (hasRoutes) {
// 如果路由已加载,直接放行
next()
} else {
try {
// 1. 获取动态路由
const accessRoutes = await permissionStore.generateRoutes()
// 2. 动态添加到 Router
accessRoutes.forEach(route => {
router.addRoute(route)
})
hasRoutes = true
// 3.关键:确保路由添加完成后再跳转
next({ ...to, replace: true })
} catch (error) {
next('/login')
}
}
}
} else {
next('/login')
}
})
记得在 src/main.js 中 import './permission' 激活路由守卫。