Vue权限控制实战:路由、菜单与按钮级三层方案
发布时间:2026/10/19 1:22:07 锦皓数字建站

简介这份资源面向Vue前端开发者与后台管理系统搭建者聚焦权限控制这一核心场景讲解如何让不同角色用户仅访问被授权的功能。内容围绕两种限制方式展开一是路由不可见借助Vue Router的meta字段存储权限列表并通过全局前置守卫beforeEach完成校验与重定向二是元素不可见通过自定义指令v-acl在元素插入时判断权限并隐藏无权按钮避免无效点击引发接口报错。资源包为1个PDF文件大小约55KB内容紧凑适合作为权限控制入门与速查参考。目前已有778人学习下载读者可从中获得可直接套用的代码示例、路由与指令两套方案的实现思路以及侧边栏导航过滤等扩展方向便于在真实项目中快速落地并进一步定制更复杂的权限管理逻辑。1. 从路由表到按钮级一套能直接落地的 Vue 权限控制方案很多团队做后台系统时权限控制一开始都是“先能跑就行”结果越往后越乱菜单写死在模板里按钮靠v-if硬编码角色字符串接口权限又另写一套判断。等到要加一个“运营只读”角色时前端得改十几个文件测试还得把每个页面点一遍。这份 Vue 权限控制资源解决的正是这个痛点——它把权限拆成路由级、菜单级、按钮级三层用统一的数据源驱动新增角色只改配置不改组件。适合正在做中后台管理系统、需要区分管理员/运营/普通用户等角色的前端开发者也适合想搞明白动态路由和指令鉴权到底怎么配合的人。下面按“先跑起来、再拆原理、最后避坑”的顺序讲每一步都能直接抄。2. 环境搭建与最小可运行骨架先把登录态和路由守卫串起来权限控制不是孤立功能它依赖登录态、用户信息、路由表三者的配合。这一章先把最小骨架搭出来让“未登录跳登录页、已登录按角色放行”这条链路先跑通后面再往里面填菜单和按钮。2.1 项目初始化与依赖选择用 Vue CLI 或 Vite 建一个 Vue 3 项目都行核心依赖只有两个vue-router负责路由pinia负责存用户角色和权限码。不引入额外权限库是因为这类库往往把逻辑包成黑匣子出问题时排查成本高而权限控制本身逻辑并不复杂自己写反而可控。# 用 Vite 创建 Vue 3 项目模板选 Vue npm create vitelatest vue-permission-demo -- --template vue cd vue-permission-demo npm install npm install vue-router4 pinia装完后目录结构建议这样组织后面所有改动都围绕这几个文件src/ router/ index.js # 路由实例与全局守卫 asyncRoutes.js # 需要按权限动态挂载的路由 store/ user.js # 用户角色、权限码、登录动作 permission.js # 路由守卫逻辑也可直接写在 router/index.js views/ Login.vue Dashboard.vue Admin.vueasyncRoutes.js和constantRoutes要分开登录页、404 页属于所有人都能访问的常量路由直接注册管理页、财务页属于动态路由登录后根据角色决定挂不挂。这个拆分是后面动态addRoute的前提混在一起会导致刷新后路由重复添加。2.2 登录态存储与路由守卫用户登录成功后后端一般返回角色标识如admin、editor和权限码数组如[user:add, user:delete]。前端把这两样存进 Pinia同时把 token 存 localStorage防止刷新丢失。// store/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , roles: [], // 角色标识如 [admin] permissions: [] // 按钮级权限码如 [user:add] }), actions: { // 登录后写入用户信息 setUserInfo({ token, roles, permissions }) { this.token token this.roles roles this.permissions permissions localStorage.setItem(token, token) }, // 退出时清空避免残留 resetUser() { this.token this.roles [] this.permissions [] localStorage.removeItem(token) } } })路由守卫写在permission.js里用router.beforeEach拦截。逻辑分三步有 token 且去登录页就放行到首页有 token 但没拉过用户信息就先拉没 token 且目标不是登录页就踢回登录页。// permission.js import router from ./router import { useUserStore } from ./store/user const whiteList [/login] // 免登录白名单 router.beforeEach(async (to, from, next) { const userStore useUserStore() if (userStore.token) { if (to.path /login) { next({ path: / }) } else { // 已有角色信息直接放行否则先拉取 if (userStore.roles.length 0) { next() } else { try { await userStore.fetchUserInfo() // 调接口拿角色和权限码 next({ ...to, replace: true }) // replace 避免历史记录多一条 } catch (e) { userStore.resetUser() next(/login?redirect${to.path}) } } } } else { if (whiteList.includes(to.path)) { next() } else { next(/login?redirect${to.path}) } } })这里有个容易翻车的点next({ ...to, replace: true })里的replace必须加否则用户点浏览器后退会回到一个“正在加载”的中间态。另外fetchUserInfo要保证只调一次可以在 store 里加个loaded标志位避免多个路由并发触发时重复请求。3. 动态路由与菜单渲染让不同角色看到不同的导航骨架跑通后核心问题来了管理员能看到“用户管理”普通编辑看不到。这一章讲怎么用router.addRoute动态挂载以及菜单怎么从同一份路由表里自动生成避免菜单和路由两处维护。3.1 路由元信息设计与动态挂载每条动态路由的meta里放两个关键字段roles表示哪些角色可访问title和icon给菜单用。这样一份配置同时驱动路由和菜单改一处两处都生效。// router/asyncRoutes.js export const asyncRoutes [ { path: /admin, component: () import(../views/Admin.vue), meta: { title: 用户管理, icon: user, roles: [admin] } }, { path: /editor, component: () import(../views/Editor.vue), meta: { title: 内容编辑, icon: edit, roles: [admin, editor] } } ]登录后根据角色过滤出可访问的路由再逐条addRoute。注意addRoute是增量添加刷新页面后路由会重置所以守卫里每次拉完用户信息都要重新挂载一遍。// 在 store 或 permission.js 中 import { asyncRoutes } from ./router/asyncRoutes function filterRoutes(routes, roles) { return routes.filter(route { if (route.meta route.meta.roles) { // 角色有交集才保留 return roles.some(role route.meta.roles.includes(role)) } return true }) } // 挂载动态路由 const accessibleRoutes filterRoutes(asyncRoutes, userStore.roles) accessibleRoutes.forEach(route router.addRoute(route))filterRoutes只做了一层过滤如果路由有children需要递归处理。常见做法是写一个递归函数对children也跑一遍同样的逻辑否则嵌套路由的子页面会漏掉权限判断。3.2 菜单组件与路由表联动菜单不要手写el-menu-item而是遍历router.options.routes或 store 里存的可访问路由用v-for渲染。这样新增页面只要加路由配置菜单自动出现。!-- components/SideMenu.vue -- template el-menu :default-active$route.path router template v-forroute in menuRoutes :keyroute.path el-menu-item :indexroute.path span{{ route.meta.title }}/span /el-menu-item /template /el-menu /template script setup import { computed } from vue import { useRouter } from vue-router const router useRouter() // 只取有 meta.title 的路由作为菜单项 const menuRoutes computed(() router.options.routes.filter(r r.meta r.meta.title) ) /scriptrouter.options.routes在动态addRoute后不会自动更新所以更稳妥的做法是把可访问路由存进 Pinia菜单从 store 读。这个细节不注意会出现“路由能访问但菜单不显示”的玄学问题。提示动态路由挂载后如果当前正停留在某个动态路由上刷新会先匹配到 404。解决办法是在守卫里挂载完路由后用next({ ...to, replace: true })重新触发一次匹配。4. 按钮级权限与自定义指令把 v-if 从模板里赶出去路由和菜单解决的是“能不能进这个页面”按钮级解决的是“进了页面能不能点这个按钮”。这一章讲怎么用自定义指令v-permission统一处理避免每个按钮都写一长串v-if。4.1 自定义指令实现与权限码校验思路很简单指令绑定的值是一个权限码指令在mounted时检查当前用户权限码数组里有没有这个值没有就把元素从 DOM 里移除。// directives/permission.js import { useUserStore } from ../store/user export const permission { mounted(el, binding) { const userStore useUserStore() const need binding.value // 如 user:add const has userStore.permissions.includes(need) if (!has) { // 直接移除比 display:none 更彻底 el.parentNode el.parentNode.removeChild(el) } } }在main.js里注册import { createApp } from vue import App from ./App.vue import { permission } from ./directives/permission const app createApp(App) app.directive(permission, permission) app.mount(#app)模板里这样用template button v-permissionuser:add新增用户/button button v-permissionuser:delete删除用户/button /templatebinding.value传的是权限码字符串如果某个按钮需要满足多个权限之一可以传数组指令里用some判断。注意指令只在mounted执行一次如果用户权限在页面运行中动态变化比如切换角色需要配合updated钩子重新判断或者直接刷新页面。4.2 权限码与后端接口的对应关系前端权限码不是随便定的要和后端接口权限保持一致。常见做法是后端在用户信息接口里返回权限码列表前端只做展示层控制真正的安全边界仍在后端。前端移除按钮只是防误操作不能替代接口鉴权。层级控制点数据来源失效后果路由级router.addRoute角色 roles页面无法访问菜单级菜单渲染过滤可访问路由菜单不显示按钮级v-permission权限码 permissions按钮被移除接口级后端鉴权后端独立校验请求被拒绝这张表建议贴在团队文档里新人一看就知道每层该改哪里。前端做三层后端做一层四层对齐才不会出现“按钮藏了但接口还能调”的漏洞。5. 避坑与排查权限控制里最容易翻车的五个地方权限控制代码量不大但涉及路由、状态、渲染时序踩坑点很集中。下面五条都是实际项目里反复出现的按“现象 → 原因 → 解决”写遇到时直接对号入座。5.1 刷新后动态路由丢失页面白屏或跳 404现象登录后能正常访问管理页一按 F5 刷新就跳到 404 或空白。 原因动态路由是运行时addRoute加的刷新后路由实例重置守卫还没重新挂载就先去匹配了。 解决在守卫里拉完用户信息、挂载完动态路由后用next({ ...to, replace: true })重新触发导航让路由重新匹配一次。同时确保fetchUserInfo在 roles 为空时才调用避免死循环。5.2 菜单不显示但路由能访问现象手动输 URL 能进管理页但左侧菜单里没有这一项。 原因菜单从router.options.routes读而addRoute不会更新这个数组。 解决把可访问路由存进 Pinia菜单从 store 读或者在挂载路由后手动把accessibleRoutes推入菜单数据源。不要依赖router.options.routes做菜单渲染。5.3 按钮指令不生效按钮仍然可见现象加了v-permission但没权限的按钮还是显示。 原因指令在mounted时执行如果此时用户权限码还没拉回来异步permissions是空数组判断自然失败。 解决确保进入页面前用户信息已加载完成守卫里await fetchUserInfo()就是这个作用。如果页面内动态改权限给指令加updated钩子重新判断或改用计算属性配合v-if。5.4 角色判断用字符串硬编码新增角色要改多处现象模板里到处是v-ifrole admin加一个“运营”角色要改十几个文件。 原因没有把角色和权限码抽象成配置判断逻辑散落在组件里。 解决统一用权限码角色到权限码的映射由后端返回。前端只判断permissions.includes(xxx)新增角色只改后端配置前端零改动。5.5 退出登录后路由未重置换账号登录串权限现象A 账号退出B 账号登录B 看到了 A 的菜单。 原因退出时只清了 token没有移除已挂载的动态路由也没有重置 store 里的 roles 和 permissions。 解决退出时调用resetUser()清空状态同时记录已添加的路由并在退出时用router.removeRoute移除或者直接location.reload()强制刷新用最简单的方式保证干净。注意router.removeRoute需要路由有name才能移除动态路由配置里最好给每条路由加name否则移除不掉。6. 进阶技巧用路由 meta 做页面级按钮权限的批量校验前面按钮级权限是逐个按钮加指令页面多了以后每个按钮都要想权限码容易漏。我后来习惯在路由meta里声明这个页面需要的权限码集合进入页面时统一校验一次缺权限的按钮直接不渲染配合指令做双重保险。具体做法是在asyncRoutes的meta里加permissions字段{ path: /admin, component: () import(../views/Admin.vue), meta: { title: 用户管理, roles: [admin], permissions: [user:add, user:delete, user:export] } }然后在页面组件里用computed生成一个can函数模板里用v-ifcan(user:add)代替指令。这样权限码集中声明在路由配置里页面里只引用改权限时只动一处。// composables/usePermission.js import { computed } from vue import { useRoute } from vue-router import { useUserStore } from ../store/user export function usePermission() { const route useRoute() const userStore useUserStore() // 当前页面声明的权限码 const pagePermissions computed(() route.meta.permissions || []) // 判断某个权限码是否同时满足页面声明 用户拥有 const can (code) { if (!pagePermissions.value.includes(code)) return false return userStore.permissions.includes(code) } return { can } }页面里这样用script setup import { usePermission } from ../composables/usePermission const { can } usePermission() /script template button v-ifcan(user:add)新增用户/button button v-ifcan(user:delete)删除用户/button /template这个方案的好处是权限码有两道校验路由 meta 里没声明的页面里写了也不生效用户没拥有的声明了也不显示。验证方法很简单用不同角色账号登录打开控制台看按钮是否按预期增减再手动改一下 store 里的permissions数组观察页面是否实时响应。如果用了v-if配合can响应是实时的如果用了指令需要刷新。从那以后我每次接权限需求都强制先画一张“角色-权限码-页面”对照表再动手写代码避免写到一半发现角色和权限码对不上。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。