主题
按钮权限与 Vue3 入门(三)
本页对应 按钮权限与 Vue3 入门,共 6 段视频,约 47 分钟。
学习目标
- 能够说明选项式API与组合式API的适用场景与取舍,并在完整构建应用中选择组合式API编写组件。
- 能够说明setup作为组合式API入口的执行时机(beforeCreate之前)与this不可用特性,并将模板所需变量或函数通过return暴露。
- 能够使用reactive和ref创建对象、数组及简单类型的响应式数据,验证修改后视图更新,并说明reactive仅支持引用类型、ref在脚本中需通过.value读写而模板中自动解包。
- 能够对比ref与reactive并合理选型,解释ref返回对象及.value机制、const不可重新赋值但可修改内部属性;能够使用computed派生数据并验证依赖更新重算、缓存特性及只读无副作用原则。
- 能够完成退出登录状态清理:触发mutation、改写菜单渲染数据、清除user模块用户信息、重置menu模块并重置路由。
知识点
这段守卫在解决什么问题
这段代码放在 router.beforeEach 里,目标不是“简单判断登录”,而是把登录态、用户信息、权限列表、动态路由和左侧菜单串起来:有 token 时,按需拉取用户权限,生成可访问菜单,动态注册路由,最后把菜单数据提交到 Vuex。这里的“触发 mutation”指最后一行 store.commit('menu/setMenuList', filterRoutes),它通过 Vuex 的 mutation 修改菜单状态。
为什么需要:左侧菜单和动态路由都依赖用户权限,不能全部写死;同时 Vuex 要求修改 state 走 mutation,便于追踪状态变化。
怎么想:先判断 token,再判断用户信息是否已加载;没有加载才请求权限,避免每次路由跳转都重复请求。
怎么做:保留笔记代码,按“取权限 → 生成菜单权限 → 过滤动态路由 → addRoute → commit”顺序阅读。
易错点:不要直接写 store.state.menu.menuList = ...;mutation 应是同步提交;commit 的 payload 要和 mutation 定义一致。
javascript
router.beforeEach(async(to, from, next) => {
const token = getCookie('park_token')
// 有token
if (token) {
next()
// 获取用户信息
if (!store.state.user.profile.id) {
// 1. 获取原始权限列表
const permissions = await store.dispatch('user/getProfile')
// 2. 获取菜单权限列表
const menuPerms = getMenuList(permissions)
console.log(menuPerms)
// 3. 通过菜单权限过滤一级和二级
const filterRoutes = getFilterRoutes(menuPerms, asyncRoutes)
console.log(filterRoutes)
// 4. addRoute动态添加
filterRoutes.forEach(route => router.addRoute(route))
// 5. 存入Vuex渲染左侧菜单
store.commit('menu/setMenuList', filterRoutes)
}
} else {
// 没有token
if (WHITE_LIST.includes(to.path)) {
next()
} else {
next('/login')
}
}
})有 token 时:先判断用户信息是否已加载
为什么需要:token 只能说明“登录过”,并不等于用户信息和权限已经拿到;而权限请求不应在每次路由跳转时重复执行。
怎么想:用 store.state.user.profile.id 作为“是否已加载用户信息”的判断条件,没有 id 才进入权限初始化。
怎么做:if (!store.state.user.profile.id) { ... } 包住后续逻辑。
易错点:profile 的结构要与 Vuex 中定义一致;await store.dispatch('user/getProfile') 的 await 不能漏,否则 permissions 可能是 Promise,后续 getMenuList 会拿到错误数据。
拉取权限并生成菜单权限
为什么需要:后端返回的权限列表可能包含按钮权限、接口权限等,菜单只取其中一部分,所以要先转换。
怎么想:先拿到原始 permissions,再用 getMenuList(permissions) 转成菜单权限 menuPerms。
怎么做:代码中先 console.log(menuPerms) 确认结构,再进入路由过滤。
易错点:getMenuList 的入参、返回值结构要和项目约定一致;不确定时就像转写稿里讲 ref 时强调的那样——先打印出来看,不要凭感觉猜。转写稿里的“赛大”应写作 setup,“绕函数”应理解为 ref 函数;这些是组合式 API 内容,本节只迁移其“打印确认结构、按框架入口修改状态”的调试思路。
过滤动态路由并 addRoute
为什么需要:asyncRoutes 是全部动态路由,用户不一定都有权限,必须按 menuPerms 过滤。
怎么想:getFilterRoutes(menuPerms, asyncRoutes) 得到当前用户可访问的一级、二级路由;再用 router.addRoute(route) 注册。
怎么做:filterRoutes.forEach(route => router.addRoute(route))。
易错点:权限字段和路由配置字段要能匹配;filterRoutes 为空时要能接受;重复进入守卫时要注意避免重复添加路由。另外,笔记代码把 next() 放在拉取权限之前,阅读时要意识到它是“先放行”;如果项目要求首次导航就命中新加路由,需要检查这个顺序是否满足需求。
提交 mutation 存入 Vuex 渲染左侧菜单
为什么需要:左侧菜单组件通常从 Vuex 读取菜单数据,commit 后状态变化可被响应式使用,也方便调试。
怎么想:filterRoutes 既是动态路由来源,也是菜单来源;注册路由后,把它提交给 menu/setMenuList。
怎么做:store.commit('menu/setMenuList', filterRoutes)。
易错点:mutation 名要带命名空间且与定义一致;mutation 内应同步修改 state;不要绕过 mutation 直接改 state;提交的数据结构要与菜单组件读取的字段一致。调试时依次打印 permissions、menuPerms、filterRoutes,确认每一步的数据形状,再定位是权限转换、路由过滤还是 mutation 提交的问题。
一、先看清这段代码在做什么
本节要改的地方只有一处,就是组件里 computed 中的 routes:
vue
computed: {
routes() {
// return this.$router.options.routes
return this.$store.state.menu.menuList
}
}这个 routes 是给模板里的菜单渲染用的,模板里大概率是 v-for="item in routes" 这样循环出菜单项。改动前,它返回的是 this.$router.options.routes,也就是路由配置文件里那份静态的路由表;改动后,注释掉了原来那行,改成返回 this.$store.state.menu.menuList,即 Vuex 里 menu 模块的 menuList。
所以这一节真正改的不是“写法”,而是菜单数据的来源:从路由配置搬到了 store 的状态里。代码只有两行,但要理解为什么这么改、改完怎么验证,得把计算属性这件事说清楚。
二、为什么要改:菜单数据不该再写死在路由表里
为什么需要:this.$router.options.routes 是创建 router 实例时传进去的那份配置,它在应用启动时就固定了。用它渲染菜单,等于“路由表里有什么,菜单就显示什么”。一旦菜单需要按登录用户、按权限、按后端下发的数据变化,静态路由表就撑不住了——你不可能在运行时去改 options.routes 来刷新菜单。
怎么想:菜单本质上是“一份数据”,不是“一份配置”。数据就应该放在状态容器里统一管理,谁需要谁去取。store 里的 menu 模块正好承担这个角色:登录后把菜单数据放进 state.menu.menuList,组件只管从 store 读,不关心它是怎么来的。这样菜单的“生产方”和“消费方”就解耦了。
怎么做:把原来读路由表的那行注释掉(保留着方便回退和对比),改成读 store。注意 state.menu.menuList 这个路径要和 store 里模块的注册方式对得上——如果 menu 是带命名空间的模块,state.menu 这一层仍然成立,因为模块的 state 都会挂到根 state 下。
易错点:路径写错是最常见的,比如模块实际叫 menus 你写了 menu,或者 store 里根本没有 menu 这个模块,此时 this.$store.state.menu 是 undefined,再取 .menuList 就直接报错。改完先在控制台打印一下 this.$store.state.menu,确认结构再往下写。
三、为什么用 computed,而不是 data 或 method
为什么需要:这里要的是“store 变了,菜单跟着变”。用 data 存一份拷贝,store 更新后组件不会知道;用 method 每次渲染都重新取,虽然结果对,但失去了缓存。
怎么想:转写稿里对计算属性的定义说得很清楚——它必须有一个前置依赖的已存在属性,基于一定的计算公式得到一个新的数据,并且保持这个公式一直在,依赖的数据发生变化时会重新计算。这里的依赖就是 this.$store.state.menu.menuList,公式就是“原样返回”。转写稿还提到一种用法叫映射计算:把 store 里的数据映射到组件上,比如从 store 里取 token,写一个计算属性直接 return this.$store.state.xxx,看起来没有“计算”,但它同样是计算属性,因为它建立了依赖关系。本节这个 routes 就属于映射计算,它没有副作用,是纯数据层面的映射。
怎么做:保持 computed 不动,只换 return 的内容。因为 store 的 state 是响应式的,menuList 一变,routes 会重新求值,模板自动更新,不需要手动刷新。
易错点:一是别在 computed 里做副作用。转写稿强调计算属性“只做数据转换”,不应该发请求、不该改 DOM、不该改其他变量,这些都属于副作用,应该放到生命周期或 watch 里。二是 computed 默认只读,别去给 this.routes 赋值,想改菜单就去改 store,而且要走 mutation,不要直接改 state。
四、缓存特性在这里意味着什么
转写稿专门演示过:计算属性第一次执行后会把结果缓存下来,模板里连续用三次,计算函数只执行一次;只有依赖变化时才重新执行。本节这个场景里,routes 在模板里可能被循环、被判断多次使用,有缓存就不会重复取值。
调试时可以利用这一点:在 routes() 里 console.log 一句,刷新页面看打印几次。如果每次渲染都打印,说明你可能把它写成了 method;如果依赖变化后不打印,说明依赖没有真正建立起来——比如你取的不是响应式数据,或者取的是某个普通变量。
五、改完不生效,按这个顺序排查
- 先打印
this.$store.state.menu.menuList,确认有数据、结构是数组。转写稿里反复讲的一个思路就是:不明白它长什么样,就把它打出来看看。 - 确认模板里用的确实是
routes,别是别的名字。转写稿里提到过一种警告:模板里用了某个属性,但实例上根本没有定义——名字写错就是这个现象。 - 确认 store 模块已注册、命名路径正确。
- 确认数据变化是通过 mutation 提交的,直接改 state 在某些情况下视图不会跟着更新。
六、易错点小结
computed里忘记return,返回undefined,模板循环直接报错。routes与data、methods里的名字重名,后者会覆盖前者。- 想“顺手”在
routes()里过滤或排序,可以,但要保证是纯计算;如果过滤依赖了别的响应式变量,那个变量也要是响应式的,否则不会重算。 - 把
routes当可写属性去push,既违反computed只读,也绕过了 Vuex 的修改流程。 - 回退时别忘了把那行注释恢复回来对比,否则排查“到底改了什么”会很费劲。
本节目标与代码位置
本节讲 user 模块中的“清除用户信息”。给定代码是一个典型的按 state 修改用户状态的方法:
javascript
clearUserInfo(state) {
// 清除Token
state.token = ''
state.profile = {}
removeCookie(TOKEN_KEY)
}从写法看,clearUserInfo(state) 的第一个参数是状态对象,函数内部没有返回值,而是直接修改 state 并调用 removeCookie。因此理解这段代码要抓住三件事:清内存状态、清持久化 Cookie、保证清理顺序和调用位置正确。转写稿主要围绕组合式 API 展开,但其中反复强调“修改响应式数据要落到具体属性上”,这一点正好对应这里的 state.token 和 state.profile,而不是把 state 整体覆盖掉。
为什么需要清除用户信息
为什么需要:用户登录后,token 和 profile 往往同时存在于状态层和 Cookie 中。如果退出登录或切换账号时只跳转页面,不清理这些数据,下一次请求可能继续携带旧 token,页面也可能短暂显示上一个用户的信息。
怎么想:清除用户信息不是“把页面关掉”,而是把用户身份相关的数据源全部复位。代码里涉及两个数据源:state 中的内存状态,以及 Cookie 中的持久化令牌。
怎么做:在 clearUserInfo 中先处理 state,再调用 removeCookie(TOKEN_KEY)。这样函数执行完后,内存和 Cookie 都回到未登录状态。
易错点:不要写成 state = {}。state 是传入的响应式对象,整体重新赋值可能切断响应式引用;应修改它内部的属性。也不要把 state.profile 清成 null,如果模板或组件按对象读取 profile.xxx,null 会引发访问错误,置为 {} 更稳。
清除 Token:state.token = ''
为什么需要:token 是请求身份凭证。退出登录后如果它还留在 state 里,请求拦截器可能继续把旧 token 带到后端。
怎么想:这里不是删除 token 属性,而是把它复位成空字符串。空字符串表示“当前没有令牌”,同时保留 token 字段,避免其他代码读取时出现 undefined。
怎么做:执行 state.token = ''。如果项目里有请求拦截器,它通常会判断 token 是否为空,再决定是否加认证头。
易错点:不要只清 Cookie 不清 state.token,否则内存里仍有旧令牌;也不要只清 state.token 不清 Cookie,否则刷新页面后可能又从 Cookie 恢复登录态。
清除用户资料:state.profile = {}
为什么需要:profile 通常保存昵称、头像、角色等用户资料。退出后如果还保留,页面头部可能继续显示旧用户信息。
怎么想:资料对象用空对象 {} 复位,表示“没有用户资料”,但结构仍然是对象,模板读取时不会因为 null 或 undefined 直接报错。
怎么做:执行 state.profile = {}。在 Vuex/Pinia 这类状态管理中,给已有属性赋新对象通常能触发响应式更新。
易错点:不要用 delete state.profile 代替置空。删除属性后,其他组件如果直接访问 state.profile.avatar 就可能报错;置为 {} 更安全。也不要只重置部分字段而漏掉整个资料对象,具体要按项目实际字段决定。
清除 Cookie:removeCookie(TOKEN_KEY)
为什么需要:Cookie 是持久化存储,页面刷新后状态层会重新初始化,但 Cookie 里的 token 可能还在。只清 state 不清 Cookie,等于“内存清了、磁盘没清”。
怎么想:TOKEN_KEY 是令牌在 Cookie 中的键名,必须和写入时使用的键名一致。removeCookie 是项目封装的删除方法,调用它才算完成持久化清理。
怎么做:在 state.token 和 state.profile 复位后调用 removeCookie(TOKEN_KEY)。顺序上先清内存再清 Cookie,可以避免中间状态被其他逻辑读取。
易错点:TOKEN_KEY 不要手写成普通字符串,否则容易和写入时的常量不一致;删除 Cookie 时如果封装方法没有处理 path、domain,可能出现“代码执行了但 Cookie 还在”的情况,需要检查 removeCookie 的实现是否与写入时匹配。
调试与调用检查
为什么需要:这段代码很短,但清理不彻底时问题往往在刷新后才暴露,所以要做一次调用后的检查。
怎么想:验证三件事:state.token 是否为空字符串,state.profile 是否为空对象,Cookie 中对应键是否消失。
怎么做:在调用 clearUserInfo 后打印 state.token、state.profile,并在浏览器开发者工具中查看 Cookie。若 Cookie 仍在,优先检查 TOKEN_KEY 和 removeCookie 的路径配置。
易错点:不要只验证页面跳转成功就认为清理完成;跳转成功不等于状态和 Cookie 都清干净。另外,本段代码只处理 token、profile 和对应 Cookie,如果项目还有其他用户态缓存,应按项目实际情况同步处理,不能凭这段代码推断。
一、这段代码要解决什么问题
javascript
import { contantsRoutes, resetRouter } from '@/router'
export default {
mutations: {
resetMenu(state) {
// 重置左侧菜单
state.menuList = contantsRoutes
// 重置路由系统
resetRouter()
}
}
}为什么需要:菜单和路由往往是一组联动的状态。用户退出登录、切换角色或权限变化后,如果只清菜单不重置路由,可能出现“左侧菜单没了,但旧路由还能访问”的不一致;如果只重置路由不恢复菜单,又可能出现页面能进但菜单为空。因此需要一个统一的“重置入口”。
怎么想:重置逻辑不要散落在组件里,而应放进 menu 模块的 mutation 中。mutation 的职责是集中修改状态,这样在 Vue Devtools 里能看到一次明确的 resetMenu 提交记录,排查时知道是谁改了 state.menuList。
怎么做:在 resetMenu(state) 中做两件事:先把左侧菜单恢复为常量路由 contantsRoutes,再调用 resetRouter() 重置路由系统。两件事放在同一个 mutation 里,保证调用方只提交一次。
易错点:不要只写 state.menuList = contantsRoutes 而漏掉 resetRouter();也不要在组件里直接 state.menuList = ...,那样绕过 mutation,状态变化不可追踪。还要确认 contantsRoutes 的导出名与源模块完全一致,不要想当然改成 constantsRoutes,除非源模块确实如此。
二、重置左侧菜单:state.menuList = contantsRoutes
为什么需要:menuList 是驱动左侧菜单渲染的数据。重置时把它赋回常量路由,相当于恢复“基础菜单集合”,避免上一次登录或上一次权限留下的菜单残留。
怎么想:这里不是“清空数组”,而是“恢复默认常量”。所以直接赋值 contantsRoutes 是最直接的写法。但要注意,这是引用赋值:如果后续有人直接修改 state.menuList 里的对象,可能影响常量本身。若项目需要按权限过滤菜单,应生成新数组,而不是在原常量上改。
怎么做:保留笔记中的赋值语句,并在调用重置流程前确认 state.menuList 当前结构。调试时可以临时打印:
javascript
resetMenu(state) {
console.log('before menuList', state.menuList)
state.menuList = contantsRoutes
console.log('after menuList', state.menuList)
resetRouter()
}转写稿里反复强调“不清楚就打印出来看看”,这里同样适用:先确认赋值前后菜单结构是否符合预期。
易错点:一是拼写错误导致导入失败;二是把常量路由直接交给业务代码随意修改;三是误以为赋值后页面一定立刻更新,实际还要确认组件是否响应了 menuList。
三、重置路由系统:resetRouter()
为什么需要:路由系统里可能已经动态添加过权限路由。退出或切换角色后,如果不移除旧路由,旧权限可能仍然可访问,形成状态残留。
怎么想:菜单重置和路由重置是两件事,不能互相替代。resetRouter() 负责路由侧清理,state.menuList = contantsRoutes 负责菜单侧恢复。二者放在同一个 mutation 中,调用方只需提交 resetMenu。
怎么做:确认 resetRouter 从 @/router 正确导入,并在菜单赋值后调用。调试时可在调用前后打印标记:
javascript
console.log('resetRouter before')
resetRouter()
console.log('resetRouter after')如果控制台先报模块导入错误,应先检查 @/router 是否导出 resetRouter,而不是继续怀疑菜单逻辑。
易错点:resetRouter() 如果内部有异步操作,mutation 本身应保持同步,不要把请求塞进 mutation;调用重置后若当前页面仍停留在旧路由,需要按项目现有流程处理跳转,不能想当然认为路由会自动回到首页;重复调用时还要确认 resetRouter 是否可重复执行。
四、模块导出、调用与调试
为什么需要:这段代码导出的是 menu 模块对象,只有通过 commit('resetMenu') 触发 mutation,重置逻辑才会执行。若命名空间或提交名写错,代码看着对但不会生效。
怎么想:先确认模块是否开启命名空间,再确认提交名。转写稿里提到“执行时机”和“自动执行”的排查思路,这里对应的是:先确认 resetMenu 是否被提交,再确认 resetRouter 是否被调用。
怎么做:在需要清理菜单和路由的流程中提交该 mutation;打开 Vue Devtools 查看 mutation 记录;在 resetMenu 内打日志;刷新页面验证菜单和路由是否都恢复。
易错点:提交名写成 menu/resetMenu 或 resetMenu 要与命名空间匹配;mutation 内不要写异步请求;不要只验证菜单显示而忽略路由访问;调试日志记得在上线前清理。
退出登录为什么要重置路由
为什么需要:退出登录不只是清 token。若只清登录凭证,不清菜单和动态路由,下一个账号登录后可能还看到上一个账号的菜单入口,甚至访问到不该出现的路由。笔记在 logout() 中增加重置菜单提交,正是为了解决“退出后状态残留”。
怎么想:把登录态拆成两部分看:凭证是“能不能进”,菜单/路由是“进来后能看到什么”。退出时两者都要回到初始状态。menu/resetMenu 从命名看是 menu 模块下的重置 mutation,调用点放在退出登录方法里,保证退出动作和菜单清理同步发生。
怎么做:在原有退出逻辑中追加提交,不把清理逻辑散落在组件里。笔记给出的改动只有一行:
javascript
logout() {
// 增加代码
this.$store.commit('menu/resetMenu')
}易错点:不要只清 token 而漏掉菜单;不要写成普通 commit('resetMenu') 而漏掉 menu/ 命名空间;不要假设 resetMenu 内部一定删了动态路由——笔记未给出内部实现,本文不虚构,调试时以退出后菜单是否清空、换账号是否残留为准。
在 logout 中提交 resetMenu
为什么需要:退出登录是用户主动结束会话的入口,放在这里提交 resetMenu,能保证“退出”与“重置菜单”是一个动作,避免用户点退出后仍停留在旧菜单状态。
怎么想:组件只负责触发退出动作,具体重置由 Vuex mutation 完成。this.$store.commit('menu/resetMenu') 表示提交 menu 命名空间模块中的 resetMenu。这样组件不直接操作菜单数据结构,职责更清楚。
怎么做:保留原有退出逻辑,在合适位置追加该行;若原方法后续还有跳转登录页等动作,应保证重置提交在跳转前执行。笔记只展示了这一行新增代码,未展示完整退出流程,因此不要补写不存在的跳转代码。
易错点:commit 拼写、模块名 menu、mutation 名 resetMenu 三者要一致;若项目使用 mapMutations 或封装 action,不能照抄成不匹配的写法;不要在该行前后随意加未验证的 router.removeRoute 等代码。
权限点对照表与菜单/路由的关系
为什么需要:退出时重置的是菜单/路由状态,而菜单能否显示、按钮能否出现,通常由权限点控制。笔记补充的对照表给出了“功能—权限点”的映射,便于核对菜单和按钮权限是否一致。
怎么想:权限点字符串是前后端约定值,不能自行改写前缀。表中既有 park:,也有 property:、parking:、pole:、sys:,说明不同业务模块前缀不同。重置菜单后,下次登录应重新按当前账号权限生成可见菜单。
怎么做:把对照表作为权限配置的参考,保留原值:
xml
添加/编辑楼宇 park:building:add_edit
楼宇管理 park:building:list
删除楼宇 park:building:remove
添加/编辑企业 park:enterprise:add_edit
企业管理 park:enterprise:list
查看企业详情 park:enterprise:query
删除企业 park:enterprise:remove
添加账单 park:propertyFee:add
物业费管理 property:propertyFee:list
查看账单详情 park:propertyFee:query
删除账单 park:propertyFee:remove
添加、续签、退租合同 park:rent:add_surrender
删除合同 park:rent:remove
添加/编辑区域 parking:area:add_edit
区域管理 parking:area:list
删除区域 parking:area:remove
添加/编辑月卡 parking:card:add_edit
月卡管理 parking:card:list
查看月卡 parking:card:query
续费月卡 parking:card:recharge
删除/批量删除月卡 parking:card:remove
停车缴费管理 parking:payment:list
添加或编辑规则 parking:rule:add_edit
计费规则管理 parking:rule:list
删除规则 parking:rule:remove
添加或者编辑一体杆 pole:info:add_edit
一体杆管理 pole:info:list
删除或批量删除一体杆 pole:info:remove
告警记录 pole:warning:list
查看详情 pole:warning:query
删除记录 pole:warning:remove
派单 pole:warning:send
添加/编辑角色 sys:role:add_edit
角色管理 sys:role:list
删除角色 sys:role:remove
添加/编辑员工 sys:user:add_edit
员工管理 sys:user:list
删除员工 sys:user:remove
重置密码 sys:user:resetPwd易错点:不要因为看到 park:propertyFee:add 和 property:propertyFee:list 就擅自统一前缀,表里原样是什么就用什么;不要用中文名称当权限判断;对照表不是路由实现,不能替代 resetMenu。
验证与易错点
为什么需要:这类改动很容易“代码加上了但页面没变化”。必须验证退出后菜单是否清空、换低权限账号登录是否还残留旧菜单。
怎么想:调试时先看提交是否执行,再看菜单状态是否变化,最后换账号看动态路由是否重新生成。笔记和转写稿没有给出 resetMenu 内部实现,因此不能虚构“它一定删除了某条路由”的细节,只能按页面表现验证。
怎么做:退出登录后观察菜单区域;重新登录不同权限账号,核对上表权限点对应的菜单/按钮是否按当前账号出现;若仍残留,检查 commit 的模块名和 mutation 名是否匹配。
易错点:只测同一账号会掩盖问题;只看控制台不看页面会漏掉菜单残留;不要把无关的 Vue3 组合式 API 内容硬套到本节路由重置中。本节改动点很小,关键是调用位置正确、命名空间正确、退出后状态确实回到初始。
代码示例
① 路由守卫中动态添加权限路由(permission.js)
js
// ---------- router/index.js ----------
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 静态路由:无需权限,登录后人人可访问
export const contantsRoutes = [
{ path: '/login', component: () => import('@/views/login') },
{ path: '/dashboard', component: () => import('@/views/dashboard') }
]
// 动态路由:需要按权限过滤后再 addRoute
export const asyncRoutes = [
{
path: '/park',
component: () => import('@/layout'),
children: [
{
path: 'building',
meta: { title: '楼宇管理', perms: ['park:building:list'] },
component: () => import('@/views/park/building')
}
]
}
]
const createRouter = () => new VueRouter({ routes: contantsRoutes })
const router = createRouter()
// 重置路由系统(Vue Router 3 通过替换 matcher 实现)
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher
}
export default router
// ---------- permission.js ----------
import router, { asyncRoutes } from '@/router'
import store from '@/store'
import { getCookie } from '@/utils/auth'
import { getMenuList, getFilterRoutes } from '@/utils/permission'
const WHITE_LIST = ['/login', '/404']
router.beforeEach(async (to, from, next) => {
const token = getCookie('park_token')
// 有token
if (token) {
next()
// 获取用户信息
if (!store.state.user.profile.id) {
// 1. 获取原始权限列表
const permissions = await store.dispatch('user/getProfile')
// 2. 获取菜单权限列表
const menuPerms = getMenuList(permissions)
console.log(menuPerms)
// 3. 通过菜单权限过滤一级和二级
const filterRoutes = getFilterRoutes(menuPerms, asyncRoutes)
console.log(filterRoutes)
// 4. addRoute动态添加
filterRoutes.forEach(route => router.addRoute(route))
// 5. 存入Vuex渲染左侧菜单
store.commit('menu/setMenuList', filterRoutes)
}
} else {
// 没有token
if (WHITE_LIST.includes(to.path)) {
next()
} else {
next('/login')
}
}
})- 关键行:
if (!store.state.user.profile.id)是幂等开关——有 profile.id 说明权限已拉过,跳过,避免每次跳转都重复addRoute(重复添加会报警告并产生重复菜单)。 - 关键行:
filterRoutes.forEach(route => router.addRoute(route))一次性把过滤后的动态路由注册进路由系统,之后$router才认得这些 path。 - 关键行:
store.commit('menu/setMenuList', filterRoutes)只存“过滤后的路由”,左侧菜单直接拿它渲染,菜单与路由天然一致。 - 易错点:笔记中
next()写在await之前,首次直接访问动态路由(如刷新/park/building)时,addRoute发生在导航确认之后,会白屏/404。稳妥写法是异步逻辑跑完后next({ ...to, replace: true })重新触发一次匹配。 - 易错点:
getMenuList/getFilterRoutes是项目内的工具函数(把park:building:list这类权限点映射成路由标识,再按标识递归过滤asyncRoutes的一级和二级),需结合权限点对照表实现,不在本段范围内。
② 退出登录时重置菜单与路由(user / menu 模块)
js
// ---------- store/modules/user.js ----------
import { getCookie, removeCookie } from '@/utils/auth'
const TOKEN_KEY = 'park_token'
export default {
namespaced: true,
state: () => ({
token: getCookie(TOKEN_KEY),
profile: {}
}),
mutations: {
clearUserInfo(state) {
// 清除Token
state.token = ''
state.profile = {}
removeCookie(TOKEN_KEY)
}
}
}
// ---------- store/modules/menu.js ----------
import { contantsRoutes, resetRouter } from '@/router'
export default {
namespaced: true,
state: () => ({
menuList: contantsRoutes
}),
mutations: {
setMenuList(state, list) {
state.menuList = list
},
resetMenu(state) {
// 重置左侧菜单
state.menuList = contantsRoutes
// 重置路由系统
resetRouter()
}
}
}
// ---------- 组件中的退出登录 ----------
export default {
methods: {
logout() {
// 增加代码
this.$store.commit('menu/resetMenu')
this.$store.commit('user/clearUserInfo')
this.$router.push('/login')
}
}
}- 关键行:
state.menuList = contantsRoutes是回到“静态路由”而不是空数组,否则重置后连登录页/首页的菜单都没了。 - 关键行:
resetRouter()必须放在resetMenu里一起做——只清 Vuex 不清路由,换账号登录时旧账号的动态路由还留在路由表里,等于越权可访问。 - 关键行:
clearUserInfo里state.profile = {}不能漏,否则profile.id还有值,下次登录!store.state.user.profile.id为 false,权限不会再拉取一次。 - 易错点:Vue Router 3 用
router.matcher = newRouter.matcher重置;Vue Router 4(Vue3)没有 matcher,要遍历router.getRoutes()逐个removeRoute。
③ 菜单渲染数据改写为 Vuex 数据(选项式 → 组合式)
vue
<!-- 选项式写法(Vue2 / Vue3 均可) -->
<script>
export default {
computed: {
routes() {
// return this.$router.options.routes
return this.$store.state.menu.menuList
}
}
}
</script>vue
<!-- 组合式写法(Vue3 <script setup>) -->
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
// computed 传入回调函数,内部 return 计算结果
const routes = computed(() => store.state.menu.menuList)
</script>
<template>
<el-menu :default-active="$route.path" router>
<template v-for="route in routes" :key="route.path">
<el-menu-item v-if="!route.children" :index="route.path">
{{ route.meta && route.meta.title }}
</el-menu-item>
<el-submenu v-else :index="route.path">
<template #title>{{ route.meta && route.meta.title }}</template>
<el-menu-item
v-for="child in route.children"
:key="child.path"
:index="`${route.path}/${child.path}`"
>
{{ child.meta.title }}
</el-menu-item>
</el-submenu>
</template>
</el-menu>
</template>- 关键行:
this.$router.options.routes只是创建路由时的初始配置,addRoute动态加进去的路由不会出现在这里,所以必须换成 Vuex 里的menuList,否则左侧菜单永远是静态那几条。 - 关键行:组合式里没有
this,取 store 要用useStore();computed()的回调必须 return,不 return 得到的是undefined。 - 易错点:
computed是只读的,直接routes.value = xxx会报错,改菜单只能通过commit('menu/setMenuList')。 - 易错点:模板里
route.meta.title要防空(route.meta && route.meta.title),静态路由常常没写meta。
常见错误
登录后首次进入菜单页白屏或 404,刷新一下又正常 现象:跳转目标页面匹配不到,控制台报 No match found。 原因:
beforeEach里先next()放行,导航已经结束才执行addRoute,目标路由此时还没注册。 修法:把next()挪到addRoute和commit之后,并用next({ ...to, replace: true })重新触发一次导航,让新路由参与匹配。切换账号后,新账号仍能看到上一个账号的菜单和页面 现象:低权限账号登录,左侧菜单少了,但直接输 URL 还能进。 原因:只清了 token 和 profile,
addRoute加进去的路由还留在 router 实例里。 修法:logout里同时commit('menu/resetMenu'),resetMenu中既重置menuList为constantRoutes,也调用resetRouter()真正移除动态路由。重复登录 / 多次进入后路由重复、菜单项重复出现 现象:菜单出现两份,或同名路由告警。 原因:每次
beforeEach都无条件执行addRoute,没有用profile.id之类的标记做“只执行一次”的守卫。 修法:用if (!store.state.user.profile.id)包裹整段拉取和添加逻辑;或在添加前先resetRouter()。重置路由后连首页、404 都进不去 现象:退出登录后所有地址都跳 login 或空白。 原因:
resetRouter只保留了不含兜底路由的常量表,通配404也没重新挂。 修法:constantRoutes里必须包含登录页、首页、404 兜底,重置时把它们重新addRoute回去;removeRoute只删动态部分,别整体重建 matcher 后忘了常量。Vue3 的
setup里写this.$router/this.$store,报 undefined 现象:Cannot read properties of undefined。 原因:setup在beforeCreate之前执行,此时this不是组件实例,拿不到任何实例属性。 修法:改用useRouter()、useStore()、useRoute()从模块导入,别在setup里碰this。脚本里改
ref忘了.value,视图不更新;或模板里多写了.value现象:赋值后页面没变,或者渲染出[object Object]。 原因:ref返回的是包装对象,脚本侧访问必须走.value,模板侧由框架自动解套。 修法:脚本里读写一律加.value,模板里一律不加;拿不准就console.log打印看结构。解构
reactive或reactive传简单类型,响应性丢失 现象:const { name } = reactive({ name: 'x' })改name页面不动;reactive(0)直接告警。 原因:解构出的是普通值,脱离了代理;reactive只接受引用类型。 修法:简单类型用ref,需要解构时用toRefs,或者干脆全项目统一用ref。computed里发请求 / 改 DOM / 改别的状态,导致请求反复发出 现象:接口被调多次,甚至死循环。 原因:computed应保持纯计算,依赖一变就重算,副作用会被反复执行。 修法:异步和副作用交给watch或生命周期钩子,computed只做数据转换并return结果。
练习
把笔记里的
beforeEach改写成不会出现“首次白屏”的版本,并用注释说明每一步为什么放在next之前。额外回答:为什么不能简单写成next()后加router.addRoute()再next()第二次?实现一个
resetRouter:要求能真正移除所有动态添加的路由,同时保证constantRoutes(含 404 兜底)在重置后依然可用。给出两种写法(removeRoute逐个删 / 记录addRoute返回的移除函数),并说明各自适用场景。用组合式 API 重写笔记里的菜单渲染:在
setup中用useStore取出menuList,写成computed返回给模板。要求不使用this,并说明这里用computed而不是普通变量的理由。用
ref和reactive各实现一次“列表过滤”:源数据[1,2,3,4,5,6],过滤出大于 3 的项。写完后回答:① 脚本里改数据哪里必须加.value;② 若把reactive的对象解构使用会发生什么,怎么修。线上问题排查:某后台“退出登录后换低权限账号登录,仍可直接输入 URL 访问高权限页面”。请写出你的排查顺序(至少 3 步),并说明最终定位在哪个环节、如何验证修复。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | Vue3-两种风格的取舍 | 07:14 |
| 2 | Vue3-组合式API入口-setup | 06:33 |
| 3 | Vue3组合式API-reactive | 06:18 |
| 4 | Vue3组合式API-ref | 05:39 |
| 5 | ref对比reactive | 06:19 |
| 6 | Vue3组合式API-computed | 15:18 |
小结
- 动态路由的三件事必须成套出现:拉权限 →
addRoute→commit菜单;少任何一步都会表现为“菜单有但进不去”或“能进但菜单空”。 next()是导航的放行闸门,任何“导航前必须完成”的准备工作都要排在它前面,必要时用next({ ...to, replace: true })重走一次。- 有“添加”就必须有“移除”:
addRoute的对称操作是removeRoute/resetRouter,登录态的清理要把 token、profile、路由、菜单四样一起清。 beforeEach里的初始化逻辑一定要有幂等守卫(如profile.id判空),否则刷新、重复登录都会让副作用叠加。- Vue3 组合式 API 的入口是
setup,它比beforeCreate还早,this不可用,一切依赖都靠显式导入的useXxx获取。 ref与reactive的差异本质是“包装对象 vs 代理对象”:脚本侧.value、模板侧自动解套,这条规则记牢能省掉大半调试时间。computed的定位是“带缓存的纯派生数据”,缓存是性能红利也是行为约束——别在里面做请求、改 DOM、改其他状态。- 遇到“为什么必须这样写”的疑惑,最快的手段是把返回值
console.log出来看结构,比背结论可靠得多。