方案:后端返回完整路由结构,前端动态组件映射
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 文件:
const modules = import.meta.glob('../views/**/*.vue')
|
第二步:Pinia 状态管理
我们需要一个 Store 来处理路由数据的获取和格式化。
关键逻辑: 将后端返回的字符串 component 映射为前端的组件对象。
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 const accessedRoutes = this.filterAsyncRoutes(remoteRoutes) this.routes = constantRoutes.concat(accessedRoutes) resolve(accessedRoutes) }) },
filterAsyncRoutes(routes) { const res = [] routes.forEach(route => { const tmp = { ...route } 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')
if (hasToken) { if (to.path === '/login') { next({ path: '/' }) } else { if (hasRoutes) { next() } else { try { const accessRoutes = await permissionStore.generateRoutes() accessRoutes.forEach(route => { router.addRoute(route) }) hasRoutes = true next({ ...to, replace: true }) } catch (error) { next('/login') } } } } else { next('/login') } })
|
记得在 src/main.js 中 import './permission' 激活路由守卫。