在后台管理系统中,不同角色的用户看到不同的菜单是基本需求。前端如果一次性注册所有路由,权限控制就只能靠路由守卫,但菜单仍然会暴露在代码中,并且路由配置臃肿。更合理的方案是:初始化时只注册公共路由(如登录页、404),用户登录后根据后端返回的权限数据,动态生成并注册路由。
Vue Router 4 提供了 addRoute() 方法来实现这一机制。
12.7.1 addRoute 的核心用法
// 添加一条新的路由记录
router.addRoute({ name: 'dashboard', path: '/dashboard', component: Dashboard })
// 也可以通过 parentName 将路由嵌套到已有路由下
router.addRoute('layout', {
path: 'settings',
name: 'settings',
component: () => import('@/views/settings/index.vue')
})
注意:
- 如果新路由的
name已经存在,addRoute不会覆盖,而是会在控制台警告。建议先检查路由是否已注册(router.hasRoute(name))或先调用removeRoute(name)再添加。 - 使用
addRoute添加的路由同样会立即生效,无需手动刷新。
12.7.2 典型实现流程
1. 路由初始配置只包含静态部分
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { staticRoutes } from './staticRoutes'
const router = createRouter({
history: createWebHistory(),
routes: staticRoutes // 只包含登录、404等公共路由
})
export default router
2. 后端接口返回的菜单数据结构
通常是一个树状结构,包含路径、名称、组件标识等:
[
{ "id": 1, "path": "/dashboard", "name": "Dashboard", "component": "dashboard", "meta": { "title": "首页" } },
{ "id": 2, "path": "/system", "name": "System", "component": "Layout",
"children": [
{ "id": 21, "path": "user", "name": "UserList", "component": "user/list", "meta": { "title": "用户管理" } },
{ "id": 22, "path": "role", "name": "RoleList", "component": "role/list", "meta": { "title": "角色管理" } }
]
}
]
3. 前端映射组件
由于动态路由在打包时无法确定组件路径,需要维护一个“组件标识 → 组件懒加载函数”的映射表:
// router/dynamicRoutesMap.js
const modules = import.meta.glob('@/views/**/*.vue') // Vite 方式,自动匹配所有 .vue
export function getComponent(componentPath) {
// componentPath 如 'dashboard' 对应 '@/views/dashboard/index.vue'
const key = `/src/views/${componentPath}/index.vue`
return modules[key] // 返回一个 () => import(...) 的函数
}
兼容 Webpack 的写法可以用 require.context 或直接定义一个对象映射。
4. 将菜单数据转为 Vue Router 路由配置
// utils/generateRoutes.js
import { getComponent } from '@/router/dynamicRoutesMap'
export function filterAsyncRoutes(menuList) {
const routes = []
menuList.forEach(menu => {
const route = {
path: menu.path,
name: menu.name,
meta: menu.meta,
redirect: menu.redirect
}
if (menu.component) {
route.component = getComponent(menu.component)
}
if (menu.children && menu.children.length) {
route.children = filterAsyncRoutes(menu.children)
}
routes.push(route)
})
return routes
}
5. 在路由守卫中触发动态注册
// router/permission.js
import router from './index'
import store from '@/store' // 假设用 Pinia 管理用户状态和路由状态
import { filterAsyncRoutes } from '@/utils/generateRoutes'
const whiteList = ['/login'] // 不需要权限的路由
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
if (userStore.token) {
// 已登录
if (to.path === '/login') {
next({ path: '/' })
} else {
const dynamicRoutesAdded = sessionStorage.getItem('dynamicRoutesAdded')
if (!dynamicRoutesAdded) {
try {
// 1. 请求菜单数据
const menus = await getMenuList()
// 2. 生成路由
const routes = filterAsyncRoutes(menus)
// 3. 动态添加路由,通常挂到某个父路由(如 Layout)下
routes.forEach(route => router.addRoute('Layout', route))
// 4. 标记已注册,避免重复请求
sessionStorage.setItem('dynamicRoutesAdded', '1')
// 5. 放行时重新进入目标路由(因为路由刚添加)
next({ ...to, replace: true })
} catch (error) {
// 清除token,跳转登录
next('/login')
}
} else {
next()
}
}
} else {
// 未登录时,只能访问白名单
if (whiteList.includes(to.path)) {
next()
} else {
next('/login')
}
}
})
刷新页面时 dynamicRoutesAdded 标记可放在 sessionStorage,这样关闭浏览器后会重新拉取,保留一定灵活性。如果服务端菜单有变化,可以强制用户重新登录或前端定期同步。
6. 用添加后的路由生成菜单
菜单通常使用 router.getRoutes() 过滤出需要展示的路由(例如带 meta.title 且 hidden !== true 的记录),再渲染为侧边栏。这样菜单和数据路由始终一致,不会出现“权限不对但还能看到菜单项”的问题。
// Sidebar.vue
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const menuRoutes = computed(() => {
// 只展示元数据中 hidden 不为 true 的子路由
return router.options.routes.find(r => r.name === 'Layout').children
.filter(r => r.meta?.title && !r.meta.hidden)
})
</script>
12.7.3 实际开发中的踩坑与建议
- 不要忘记在退出登录时重置动态路由。否则上一个用户的权限可能会残留:
function resetRouter() {
const router = useRouter()
router.getRoutes().forEach(route => {
if (route.name && !['Login', '404', 'Layout'].includes(route.name)) {
router.removeRoute(route.name)
}
})
sessionStorage.removeItem('dynamicRoutesAdded')
}
- 路由重复添加问题:每次守卫执行时先检查
router.hasRoute(name),如果已存在可以跳过,但务必与其他逻辑(如退出重登的场景)配合好。
- 组件失效:如果使用
import.meta.glob但路径匹配不正确,会导致组件加载失败,建议在开发时打印一次映射表跟后端返回的component值做比对。
- 与静态路由共存:静态路由(如
Layout)必须在创建 Router 时就定义好,否则addRoute(parentName)会因为找不到父路由而报错。
- 类型安全:如果是 TypeScript 项目,给菜单数据和路由配置定义清晰的 Interface,可以避免很多运行时错误。
动态路由配合 addRoute 实现了真正意义上的“按权限加载菜单”,既保证了安全(未授权页面连路由记录都不存在),又让菜单渲染与路由彻底解耦,是后台管理系统中最实用的权限控制方案之一。