在中后台管理系统中,基于角色的权限控制几乎是绕不开的需求。前端需要根据用户身份动态决定能访问哪些页面、看到哪些菜单。Vue Router 4 配合 Vue 3 的组合式 API,为这类场景提供了清晰的实现路径。本文将从路由结构设计、动态路由生成、导航守卫控制到按钮级权限,给出一套可落地的完整方案。
一、整体设计思路
权限控制的核心逻辑可以概括为三步:
- 路由分层:将路由分为「静态路由」和「动态路由」两类。静态路由是所有用户都能访问的页面(如登录页、404 页),动态路由则根据用户权限按需添加。
- 登录后拉取权限:用户登录成功后,前端向后端请求用户信息与权限标识,据此计算出该用户可访问的路由表。
- 动态注册路由:通过
router.addRoute()将计算好的路由动态添加到路由器实例中,同时生成对应的菜单。
这种方式的优势在于:未授权的路由根本不会被注册到路由器中,用户即使手动输入 URL 也无法访问,安全性比单纯在守卫里判断更高。
二、路由结构设计
建议将所有路由集中管理,每个路由的 meta 字段承载权限信息:
// router/routes.js
export const constantRoutes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { title: '登录', hidden: true }
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/404.vue'),
meta: { title: '404', hidden: true }
}
]
export const asyncRoutes = [
{
path: '/',
component: () => import('@/layout/index.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '仪表盘', icon: 'dashboard', roles: ['admin', 'editor'] }
}
]
},
{
path: '/system',
component: () => import('@/layout/index.vue'),
meta: { title: '系统管理', icon: 'setting', roles: ['admin'] },
children: [
{
path: 'user',
name: 'UserManage',
component: () => import('@/views/system/user.vue'),
meta: { title: '用户管理', roles: ['admin'] }
},
{
path: 'role',
name: 'RoleManage',
component: () => import('@/views/system/role.vue'),
meta: { title: '角色管理', roles: ['admin'] }
}
]
}
]
关键约定:
meta.roles声明该路由允许访问的角色列表,不写则表示所有已登录用户可访问。meta.hidden控制该路由是否在侧边菜单中隐藏。- 父级路由的
roles应包含所有子路由的角色并集,避免菜单渲染时出现空目录。
三、动态路由生成与注册
核心工具函数是根据用户角色过滤路由表:
// utils/permission.js
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
}
return true
}
在 Pinia store 中管理路由状态:
// stores/permission.js
import { defineStore } from 'pinia'
import { constantRoutes, asyncRoutes } from '@/router/routes'
import { filterAsyncRoutes } from '@/utils/permission'
export const usePermissionStore = defineStore('permission', {
state: () => ({
routes: [],
addRoutes: []
}),
actions: {
generateRoutes(roles) {
return new Promise(resolve => {
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
this.addRoutes = accessedRoutes
this.routes = constantRoutes.concat(accessedRoutes)
resolve(accessedRoutes)
})
}
}
})
注意:Vue Router 4 中
addRoute返回一个移除函数,在需要重置路由(如切换账号)时非常有用。
四、导航守卫:串联整个流程
全局前置守卫负责判断登录状态与路由注册状态:
// permission.js
import router from '@/router'
import { useUserStore } from '@/stores/user'
import { usePermissionStore } from '@/stores/permission'
const whiteList = ['/login', '/404']
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
if (userStore.token) {
if (to.path === '/login') {
next({ path: '/' })
} else {
// 已获取用户信息,直接放行
if (userStore.roles.length > 0) {
next()
} else {
try {
const { roles } = await userStore.getInfo()
const accessRoutes = await permissionStore.generateRoutes(roles)
accessRoutes.forEach(route => router.addRoute(route))
// 关键:replace 确保 addRoute 后重新匹配
next({ ...to, replace: true })
} catch (err) {
userStore.resetToken()
next(`/login?redirect=${to.path}`)
}
}
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
}
})
这里有两个容易踩坑的点:
next({ ...to, replace: true }):动态添加路由后,必须重新触发一次导航,否则当前导航仍匹配的是旧路由表,可能落到 404。- 兜底 404 路由:必须在动态路由添加完成后,再追加
{ path: '/:pathMatch(.*)*', redirect: '/404' },否则会导致未注册路由被误匹配。
五、菜单渲染与按钮级权限
侧边栏菜单直接基于 permissionStore.routes 递归渲染即可,过滤掉 meta.hidden 为 true 的项。
按钮级权限则通过自定义指令实现:
// directives/permission.js
import { useUserStore } from '@/stores/user'
export const permission = {
mounted(el, binding) {
const userStore = useUserStore()
const { value } = binding
if (value && value.length) {
const hasPerm = userStore.permissions.some(p => value.includes(p))
if (!hasPerm) {
el.parentNode?.removeChild(el)
}
}
}
}
使用方式:
<template>
<el-button v-permission="['user:delete']">删除</el-button>
</template>
六、几个实践建议
第一,动态路由不要持久化到本地。 每次刷新页面都应重新拉取用户信息并生成路由,否则角色变更后会出现权限不一致。
第二,退出登录时重置路由。 保存 addRoute 返回的移除函数,在登出时逐一调用,或直接 location.reload() 简单粗暴地重置。
第三,后端才是权限的最终防线。 前端路由控制只解决「用户看不到」,接口层面的鉴权必须由后端完成,前端方案是体验优化而非安全边界。
第四,角色与权限标识分离。 角色(role)用于路由级控制,权限点(permission)用于按钮级控制,两者粒度不同,混用会让后期维护变得困难。
小结
Vue Router 4 的动态路由方案本质上是「按需注册 + 守卫串联 + 状态管理」三者的配合。把路由分层设计清楚,用 meta 承载权限元信息,在守卫中完成异步注册与重定向,再配合菜单渲染和按钮指令,就能覆盖绝大多数中后台系统的权限需求。方案本身不复杂,真正需要留心的是那些边界情况——刷新、登出、404 兜底、路由重匹配——处理好这些细节,整套权限体系才算真正稳固。
未经允许不得转载:任鹏个人博客 » Vue Router 4 动态路由与权限控制的最佳实践方案


朋友圈点赞图在线生成源码