1. 为什么说路由守卫是后台管理系统绕不开的坎做 Vue3 后台管理系统尤其是涉及到登录、权限、角色这些词的项目路由守卫基本是你躲不掉的硬需求。我最早接触路由守卫是在 Vue2 时代那时候用beforeEach写一堆逻辑虽然能用但总觉得别扭。到了 Vue3组合式 API 加上vue-router4的全面重构路由守卫的写法和使用场景都有了不小的变化。先说清楚一个概念路由守卫不是 Vue3 的新东西它是 vue-router 本身就有的能力只是在 Vue3 生态里写法更灵活类型推导更舒服配合组合式 API 用起来很顺手。但它的核心作用一直没有变——在路由跳转之前、之后或者被复用的时候拦截这次导航执行你的逻辑决定让不让它继续。那权限验证为什么要靠它很简单前端不管怎么做权限控制最终能防的都是“不懂技术的人”真正要防的人压根不看你前端页面。但路由守卫存在的意义不是做绝对安全而是做用户体验和路由可达性的控制。比如一个未登录用户手抖输入了/admin/settings如果没有守卫页面会白屏或者报错因为路由组件可能依赖于用户数据如果有守卫直接一脚踢回登录页顺便弹个提示这就是它最典型的应用场景。这篇文章不会只讲 API 怎么用我会把路由守卫的完整应用链路拆开从基础写法到权限模型设计再到动态路由加载、刷新页面状态丢失这些实战坑一步步跟你说清楚。适合正在做 Vue3 后台项目、或者准备面试被问到“路由守卫你怎么做权限控制”的朋友当成一份可以直接抄作业的参考。2. 路由守卫的三种形态和触发时机2.1 全局守卫beforeEach、beforeResolve、afterEachvue-router4 里保留了几个全局守卫其中最常用的是beforeEach。它接收两个参数to是要去的路由from是当前离开的路由返回值可以是true、false或者一个路由地址。返回true表示放行返回false表示取消这次导航返回一个路由地址则表示重定向到那个地址。这段代码几乎每个后台项目都有// router/index.js import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [] }) router.beforeEach((to, from, next) { // 这里写你的拦截逻辑 next() }) export default router注意一个细节Vue3 的 vue-router4 里next这个参数是可选的。官方推荐直接返回true/false/ 路由地址而不是调用next()。但你写next()也不会报错兼容性还在。我个人的习惯是统一用返回值因为代码更干净也避免不小心调用两次next()导致路由跳转异常。beforeResolve和beforeEach很相似区别在于它会在所有组件内的守卫执行完之后才触发也就是说此时路由对应的组件已经被确认了。这个守卫适合做一些需要“确保所有组件就绪”的逻辑比如全局的 loading 关闭。afterEach没有拦截能力它只是在导航确认后触发通常用来做页面标题修改、埋点统计、滚动条复位这些事。2.2 路由独享守卫与组件内守卫除了全局的vue-router 还支持在某个路由配置上单独挂一个beforeEnter只在进入这个路由时触发。适合给某些特殊页面加单独拦截比如只有管理员能进的“系统监控”页面。组件内守卫则写在组件里有onBeforeRouteUpdate、onBeforeRouteLeave等几个常用于处理表单未保存离开确认、列表页参数变化重新请求数据这类场景。这三种守卫的执行顺序是固定的全局beforeEach→ 路由独享beforeEnter→ 组件内守卫 → 全局beforeResolve→ 全局afterEach。搞清楚这个顺序对排查问题特别有帮助。3. 权限模型的选型后端返回还是前端写死3.1 常见权限模型对比做权限验证前先得想明白一个问题权限数据从哪来我在项目里见过三种做法方案实现方式优点缺点前端写死在路由 meta 里写死角色守卫里判断角色简单直接适合小项目权限调整要发版不灵活后端返回角色登录后返回当前用户角色前端根据角色过滤路由角色灵活后端可控制需要维护角色和路由的映射表后端返回菜单/按钮码登录后返回当前用户拥有的菜单、按钮权限码权限粒度最细前端逻辑最复杂动态路由是标配我个人的判断是如果项目是给内部几十个人用的管理系统角色控制在个位数前端写死完全够用。但只要是稍微正规一点的系统尤其是客户会提“给这个用户加个角色”“这个菜单不要给他看”这种需求请直接上后端返回方案。因为权限的本质是“数据”数据应该由后端统一管理前端只是做展示层的控制。3.2 基于角色的动态路由设计动态路由的核心思路是前端把路由拆成两部分——公共路由和权限路由。公共路由在createRouter时直接注册权限路由在用户登录后根据其角色或权限码筛选出对应的路由通过router.addRoute()动态添加。这个过程在后台管理系统里有一个很典型的实现路径// 权限路由定义 const permissionRoutes [ { path: /admin, component: Layout, meta: { title: 系统管理, icon: setting }, children: [ { path: user, component: () import(/views/admin/User.vue), meta: { title: 用户管理, roles: [admin] } }, { path: role, component: () import(/views/admin/Role.vue), meta: { title: 角色管理, roles: [admin, operator] } } ] } ]然后在守卫里做筛选只留下当前用户有权限访问的路由再addRoute进去。有一个很关键的细节router.addRoute()以后如果用户下次登录换了一个角色你怎么把之前添加的路由移除vue-router4提供了router.removeRoute(name)但你需要记住之前添加的路由名称。更好的方案是在退出登录时直接用location.reload()刷新页面让整个应用重新初始化所有动态添加的路由自然清空。这个方案虽然看起来“粗暴”但实际项目中非常实用避免了无数状态残留的问题。4. 完整的权限验证流程与代码实现4.1 登录态检查token 存在不代表已登录我把整套权限验证拆成了几个步骤每一步都有它的目的第一步检查to.path是否在白名单里比如/login、/register、/404这些页面不需要登录就能访问。第二步检查本地有没有 token。有 token 再看是不是去登录页如果是说明已经登录了直接跳首页如果没有 token 且目标页面需要权限跳登录页并带上redirect参数。第三步有 token 的情况下还要确认用户信息是否完整。因为刷新页面后 Vuex 或者 Pinia 里的用户信息会丢失此时需要重新请求用户信息拿到角色、权限码后动态生成路由。第四步路由生成完毕后用next({ ...to, replace: true })重新进入目标路由让新的路由表生效。这个过程涉及异步操作守卫函数要返回true或重定向地址异步逻辑要处理好。看一下核心实现// src/router/guard.js export function setupRouterGuard(router, store) { const whiteList [/login, /404] router.beforeEach(async (to) { // 1. 白名单直接放行 if (whiteList.includes(to.path)) { return true } // 2. 没有 token踢回登录页 const token store.state.user.token if (!token) { return { path: /login, query: { redirect: to.fullPath } } } // 3. 有 token 但没有用户信息重新拉取 if (!store.state.user.userInfo) { try { const userInfo await store.dispatch(user/getUserInfo) // 4. 根据用户权限生成动态路由 const routes await store.dispatch(permission/generateRoutes, userInfo.roles) routes.forEach(route router.addRoute(route)) // 重新进入当前路由让新路由表生效 return { ...to, replace: true } } catch (error) { // 拉取失败可能是 token 过期清除后跳登录 await store.dispatch(user/resetToken) return { path: /login, query: { redirect: to.fullPath } } } } return true }) }这段代码里有两个地方值得单独说明。第一个是return { ...to, replace: true }这是动态路由的标准做法。因为addRoute之后当前要跳转的路由在旧路由表里可能还是未注册状态直接放行会 404重新进入一次让新路由表接管就顺畅了。第二个是try...catch包裹了获取用户信息的逻辑。真实项目中 token 失效是常态后端返回 401 时我们不能让它卡死在守卫里必须要清 token 跳登录页。有些同学会在每个请求拦截器里处理 401但守卫这里也要兜底双保险。4.2 按钮级权限不只是页面级路由守卫解决的是“页面能不能进”的问题但实际业务里一个用户进了页面他能不能看到某个按钮、能不能点击某个操作这是更精细的权限控制。做法通常是后端返回一组权限标识比如user:add、user:delete前端做一个自定义指令v-permission用的时候在按钮上写el-button v-permissionuser:add新增用户/el-button自定义指令内部判断当前用户的权限码列表里有没有这个标识没有就移除这个 DOM 元素。这样按钮级权限和路由级权限就解耦了路由层面管“能不能看到页面”指令层面管“能不能看到按钮”。不过要注意按钮级权限不建议写在路由守卫里因为路由守卫是全局性的它只做页面的可达性控制写死按钮判断会让逻辑变得很乱。自定义指令虽然好用但也能被绕过真正的安全校验必须后端做一次。前端这些都只是体验层面的东西。5. 权限验证中常见的坑与排查思路5.1 刷新页面后路由消失的问题这个问题我见得太多了用户登录后一切正常一按 F5直接 404 或者跳到登录页。原因是动态路由只存在内存里刷新后 store 清空、路由表恢复成初始状态而你当前的地址/admin/user在初始路由表里根本不存在vue-router 匹配不到就给你 404 了。解决方式就是我在上面第 4 节里的写法——进入守卫时检查用户信息如果没有就重新拉取并重新生成动态路由。但这里还有一个小坑重新生成路由后你用return { ...to, replace: true }重进一次可如果目标路由是动态添加的子路由这个操作在部分浏览器上有极小的概率会闪现一下白屏或者加载条闪烁。实测下来影响不大主要是控制台可能有些警告不影响使用。5.2 死循环跳转的问题重定向地址没处理干净守卫里最常见的报错就是Maximum call stack size exceeded一般是重定向写成了死循环。比如你在守卫里判断没有 token 就跳/login而/login的路径不在白名单里那就会反复跳转。或者你写了类似这样的逻辑if (!token) { return /login }如果/login这个页面也触发了同一个守卫而且to.path不是/login又不是白名单就会出现无限循环。所以白名单的配置一定要正确/login必须放进去。另外一个问题是redirect参数。有些同学会在登录页拿到redirect后直接用router.push(redirect)但redirect可能包含一些特殊字符需要做一遍decodeURIComponent解码。不然链接带上中文参数的时候跳转会失败或者产生奇怪的报错。5.3 动态添加路由后路由重复注册当你退出登录后没有刷新页面而是直接清掉 token 再登录另一个账号之前addRoute的路由还在再添加一遍就会报警告[vue-router] Duplicate named routes。这会影响后续的导航性能严重的会让某些路由无法匹配到正确的组件。最佳实践还是我之前说的退出登录直接location.reload()这是最简单可靠的方案。如果你不希望刷新整个页面那就得维护一个动态路由列表退出时逐个removeRoute但这对列表里存在嵌套关系的情况要特别小心处理父子关系很容易漏。5.4 异步路由加载导致的白屏闪烁动态路由的组件如果是用() import(...)的方式加载的在首次进入时有一个网络加载的过程。如果这时候没有全局的 loading 反馈用户会感觉点了菜单没反应过了几百毫秒才跳转。常见的做法是在start()中开启一个全局进度条afterEach里done()。比如很多公司项目里用的nprogress就是这个原理。这里有一个细节动态路由的组件加载失败也要处理比如网络断了、页面代码有误最好在router.onError()里捕获一下给用户一个友好提示而不是停留在白屏状态。6. 从需求到落地的完整代码结构6.1 路由模块的文件组织一个稍微大一点的后台项目文件组织一般是这样的src/router/ index.ts # 创建 路由实例 routes.ts # 静态路由表constantRoutes dynamic.ts # 权限路由表asyncRoutes guard.ts # 路由守卫逻辑index.ts里只做一件事创建 router并且把它export出去。routes.ts放不需要权限的页面比如登录、首页、404。dynamic.ts放需要权限的页面每个路由的meta里声明需要的权限或者角色。guard.ts处理拦截逻辑。这样做的好处是职责清晰别人接手你的代码先看routes.ts就知道有哪些页面不用登录看dynamic.ts就知道权限系统长什么样看guard.ts就知道整个验证流程是怎么办的。不是随便乱分的而是按照“静态、动态、拦截”这三个纬度去切分的。6.2 权限 store 的模块设计配合 Pinia 使用的时候权限相关的状态放在一个专门的 store 里。它至少要做三件事记录当前用户的权限码列表/角色列表、保存动态生成的路由表、提供一个generateRoutes的 action 来执行筛选逻辑。// store/modules/permission.js export const usePermissionStore defineStore(permission, { state: () ({ routes: [], dynamicRoutes: [] }), actions: { generateRoutes(roles) { const accessedRoutes filterDynamicRoutes(dynamicRoutes, roles) this.dynamicRoutes accessedRoutes return accessedRoutes } } })筛选逻辑的核心是一个递归函数遍历权限路由表把当前用户没有权限的路由过滤掉。要注意处理嵌套路由的情况如果一个父路由的子路由全被过滤了父路由本身也就没必要保留了。这个筛选函数是纯函数不依赖 Vue 的响应式系统抽到一个独立的utils文件里最合适方便单测。我在项目里见过把筛选逻辑写在组件内部然后又想在别的地方复用的尴尬场景抽出来就清爽多了。7. 路由守卫实际项目中的细节优化7.1 页面标题随路由变化这个功能虽然简单但很多新人在做守卫时会忽略。vue-router 的afterEach里可以读取to.meta.title直接赋值给document.title。结合动态路由title 最好在定义路由时就写在meta里全局统一管理。7.2 动态路由的组件加载失败处理在实际部署中前端代码更新后用户浏览器里还留着旧的 chunk 文件哈希此时跳转新页面import()请求旧文件会 404组件加载失败。这个场景在路由守卫里很难处理但我们要在router.onError里做一个兜底检测到是 chunk 加载失败后刷新页面重新拉取最新文件让用户自动恢复。router.onError((error) { if (error.message.includes(Failed to fetch dynamically imported module)) { window.location.reload() } })这个优化点常常被忽略但实际用户体验提升很明显。不然一个后台管理系统的用户在某个版本更新后点菜单直接就白屏只能手动 F5这个体验就很差了。7.3 面包屑导航与路由的联动面包屑一般都依赖当前路由的matched数组来生成。使用动态路由后matched数组在首次进入路由时可能没有完整的数据需要等addRoute完成之后再去读取。如果你的项目用了动态路由面包屑组件建议用router.currentRoute.value.matched实时计算而不是在组件里缓存一份不然权限不同的人看到的面包屑会错乱。8. 几种常见需求场景的守卫写法8.1 登录后不允许再进登录页已登录用户访问/login时直接重定向到首页。这个判断要放在拿到 token 之后否则会破坏登录页本身。if (to.path /login) { if (token) { return { path: /dashboard } } return true }8.2 只有管理员能访问的页面如果只有一个超级管理员角色直接在路由meta里写死roles: [admin]是最省事的。如果有多个角色但权限逻辑并不复杂也可以用meta.roles数组做包含判断。8.3 访客模式只读页面有些系统有“访客模式”登录的是短期有效的链接只能看数据不能操作。这种需求通常不靠路由守卫硬控而是后端在接口层限制。前端路由守卫只需要保证访客能进只读页面不能进有写操作的页面。实现上还是meta标记判断当前用户是不是访客角色是访客就拦截写页面。9. 面试官爱问的三个路由守卫问题多说几句面试相关的东西因为很多读者是为了面试来看这个主题的。除了“你项目里路由守卫怎么做的”之外还有几个高频面试题值得留意。第一个beforeEach能不能用async/await能而且官方推荐在需要异步获取数据时这么干。守卫函数内部 await 结束后返回结果即可。第二个addRoute之后为什么还需要replace重新跳一次因为新注册的路由不会影响当前正在进行的这次导航你需要手动触发一下让新的路由表生效。第三个动态路由和静态路由你怎么组织组织方式不一定要统一答案但思路一定要清晰静态路由是任何人可见的动态路由是登录后用权限筛选出来的。你能把自己的方案自圆其说并且说出为什么适合你当前的项目这比背一个标准答案有用得多。10. 顺着这条路还能继续拓展什么路由守卫从来不是孤立的知识点它是整个前端权限体系的一环。如果你已经掌握了本章的内容下一步建议去看 Vue Router 官方文档里关于router.beforeResolve的进阶用法再配合自定义指令做按钮级权限这一套搞下来后台管理系统的权限控制基本就过关了。最后分享一个我在实际项目里总结的小技巧不要在守卫里写太多业务逻辑。守卫的作用是拦截不等于把所有的校验都塞进去。如果业务复杂可以把校验逻辑抽成一个独立的函数在守卫里只做调用这样测试的时候也能单独测。我见过有人把十几层 if 嵌套全写在beforeEach里后面想加一个需求压根不敢动改一处爆两处。守卫的代码越短越好长的一定要拆出来这是很多踩过坑的人一致的感受。
企业数字化 ERP 产品动态
相关推荐
AI批量测短视频钩子:3秒定生死的工业化创作流程 “3秒定生死”这句话,做短视频和短剧的人应该都不陌生。用户划不划走,基本就在开局那一瞬间决定。过去定开场钩子,全靠编导的网感和经验,一条片子行不行,先拍出来再说,不行就换,效率低ÿ… · 2026/9/24 20:37:22
《动手学大模型》48.2k star教程:从原理到微调部署的完整实操指南 我最早注意到《动手学大模型》这套教程,是在一次内部技术分享会上。当时同事把上海交通大学的开源仓库投到屏幕上,我看到 GitHub 标星已经冲到 48.2k,第一反应是“又一份被收藏吃灰的豪华资料”。真正改变我判断的,是我跟着 noteb… · 2026/9/24 20:37:22
动手学大模型:从微调到部署的工程实践指南 在大模型这波浪潮里,最不缺的就是各种教程,但多数要么偏理论、看完还是不会写代码,要么就是零散的博客,没有一个完整的学习闭环。所以当我第一次看到上交大开源的这套《动手学大模型》时,确实有点“终于等到一套能跟着… · 2026/9/24 20:37:22
Windows驱动签名全攻略:从自签证书到企业CA批量部署 碰到驱动装不上、签名报错,很多人第一反应就是进高级启动按F7禁用驱动强制签名,或者在命令行里敲一句bcdedit /set testsigning on。说实话,如果是自己机器上折腾,这两种方法确实能快速解决问题,但放到企业内网、批量部… · 2026/9/24 21:11:47
切缝药包聚能爆破LS-DYNA模拟:k文件建模与调试指南 切缝药包的k文件我前前后后调了一个多月,中间踩了不少坑,也把LS-DYNA里和爆破相关的关键字基本翻了个遍。最近刚好有人问起切缝药包聚能爆破的模拟怎么做,索性把这套东西系统整理出来,从k文件结构到材料参数再到调试心得ÿ… · 2026/9/24 21:11:47
创作纪念日复盘指南:从数据分析到内容系统,创作者如何校准年度方向 我创作这三年,真正让我停下来认真想“我到底在做什么”的时刻,不是涨粉多少、不是哪篇爆了,而是平台弹出一张卡片,上面写着“今天是你的创作纪念日”。那一刻我才意识到,原来我已经在这个领域里持续输出了整整三年。创… · 2026/9/24 21:11:47
从灵感到数据:智能家居内容创作一周年复盘 1. 这个纪念日,其实是我稀里糊涂开始的说句实话,真正的创作纪念日,我一开始压根没记住。是在某天打开后台,看到系统推送的“满一周年”提示,才意识到自己已经在这个账号上写了整整一年的东西。一年前的我,和… · 2026/9/24 21:11:47
G1垃圾回收器深度解析:从Region机制到停顿调优实战 做线上服务的人,基本都绕不开GC调优这个坎。前面一篇聊了CMS和Parallel这类经典回收器,这篇专门说现在的默认主角——G1。我自己的一个支付网关服务,堆内存48G,原本跑在CMS上,一到业务高峰老年代就开始抖动,… · 2026/9/24 21:11:47
基于Transformer的皮肤病变分割毕业设计:Swin-UNet实战与优化 简介:本资源面向计算机视觉方向的毕业设计学生与深度学习入门者,提供一套基于Transformer的语义分割完整实现方案,重点解决皮肤病变区域的像素级分割问题,适用于医学图像分析场景。压缩包共约2000个文件,整体59.39MB&a… · 2026/9/24 21:11:40
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44