主题
按钮权限与 Vue3 入门(一)
本页对应 按钮权限与 Vue3 入门,共 6 段视频,约 1 小时 8 分。
学习目标
- 能够复述基于 RBAC 的菜单权限生成流程,并说明角色管理、员工管理与用户权限数据之间的绑定关系。
- 能够在退出登录时清空动态路由记录及 Vuex 中的菜单和用户权限数据,并验证切换账号后菜单不再复用上一用户缓存。
- 能够注册并启用全局按钮权限指令,在指令钩子中获取绑定元素与权限标识,完成按钮显示/隐藏控制。
- 能够根据当前用户
permissions判断按钮权限标识是否存在,并验证有权限按钮显示、无权限按钮被移除。 - 能够使用
render函数及h函数创建基础模板结构,说明标签名、属性配置和子节点三个参数的作用。 - 能够识别适合使用
render函数的场景,并用动态标签名示例说明其相比模板枚举的灵活性。
知识点
为什么需要把“获取用户信息”封装成接口
在权限控制流程里,第一步就是拿到当前用户的权限数据。转写稿里反复提到“调接口拿到 permissions”,并强调这个数据会继续参与后面的动态路由过滤、左侧菜单渲染。也就是说,页面组件不应该自己到处写 url、method,否则同一个接口一旦地址或请求方式变化,就要满项目查找修改。
“怎么想”很简单:把接口请求收敛到 API 层。业务代码只关心“我要获取用户信息”,不关心请求细节。这样命名清晰、复用方便,也便于统一调试。转写稿中提到的“调 z 口”应理解为“调接口”,“permission 字段”按项目实际字段应写作 permissions。
“怎么做”:一个接口对应一个导出函数,函数内部返回 request(...) 的结果。注意这里必须 return,因为调用方通常需要拿到 Promise,再通过 await 或 .then 取返回数据。
本节笔记代码如下,保留原样:
javascript
/**
* @description: 获取用户信息
* @param {*} data {}
* @return {*} promise
*/
export function getProfileAPI() {
return request({
url: '/park/user/profile',
method: 'GET'
})
}易错点:函数名要能表达业务含义;url 不要拼错;method 用大写 GET;不要漏写 return,否则调用处拿到 undefined,后续取 permissions 就会报错。
怎么在权限流程里理解这个接口
转写稿的调试过程是:切换到刚配置好的员工账号,刷新页面,打开检查工具,查看“当前用户”的数据,在 permissions 字段里看到刚才通过角色分配出来的两个权限数据。这个过程的本质就是:getProfileAPI 负责把后端返回的用户信息取回来,后续逻辑再从返回结果里读 permissions。
“怎么想”:封装接口不是终点,而是数据流的起点。接口返回 Promise,调用方拿到结果后,才进入权限数据过滤。转写稿里说“第一步调用 action 函数获取当前用户权限数据”,这里可以理解为:action 或页面逻辑调用 getProfileAPI(),再把结果保存到用户状态中。注意不要编造具体响应结构,调试时以实际返回为准;转写稿明确看到的是 permissions 字段。
“怎么做”:调用处应检查 Promise 的结果,确认 permissions 是否存在。若接口失败,要有错误处理;若字段为空,要能区分“没有权限”和“接口没返回”。
易错点:不要把接口函数写成组件内部方法,否则复用性差;不要在 API 函数里直接操作 store 或路由,保持职责单一;调试时先看网络请求的 url、method、返回体,再看业务字段。
调试与常见问题
结合转写稿的调试思路,排查顺序应是:先确认请求是否发出,再确认 url 是否为 /park/user/profile,method 是否为 GET,最后看返回对象里有没有 permissions。转写稿里还提到切换用户后左侧菜单仍显示旧数据的问题,那是路由缓存问题,不是 getProfileAPI 本身的问题;但接口返回的数据是否正确,是后续判断的基础。
“怎么想”:接口封装层要稳定,业务层才好排查。若页面拿不到权限,先排除接口层,再排查 store、路由过滤和菜单渲染。
“怎么做”:在调用 getProfileAPI() 的地方打印或断点查看返回结果;确认 request 已正确导入;确认导出函数被正确引入。
易错点:同音字纠正——“接口”不要写成“z 口”,“路由”不要写成“漏有”,“实例对象”不要写成“实力对象”;permissions 不要漏 s。封装接口看似只有几行,但它是权限链路的第一环,写稳了,后面的过滤和渲染才有可靠数据源。
这个 Vuex 模块解决什么问题
为什么需要:登录后要拿到当前用户的权限数据,左侧菜单、按钮权限指令等多个地方都要用。如果每个组件都单独调一次 getProfileAPI,请求会重复,数据也容易不同步。把用户信息放进 Vuex,做成带命名空间的模块,就能集中请求、集中保存、集中清空。
怎么想:state 存当前用户信息 profile;mutations 只做同步赋值;actions 负责异步请求接口,拿到结果后再 commit。加 namespaced: true,避免不同模块的 getProfile、setProfile 重名。
怎么做:本节笔记代码如下:
vue
import { getProfileAPI } from '@/apis/user'
export default {
namespaced: true,
state: () => {
return {
profile: {}
}
},
mutations: {
setProfile(state, profile) {
state.profile = profile
}
},
actions: {
async getProfile(ctx) {
const res = await getProfileAPI()
ctx.commit('setProfile', res.data)
}
}
}易错点:state 要用函数返回对象,避免模块间共享同一份引用;不要在 action 里直接改 state.profile,要走 commit;模块开启命名空间后,组件里通常要 dispatch('user/getProfile'),模块内部 commit('setProfile') 不用再加模块名。
从权限数据流转看 Vuex 的位置
为什么需要:转写稿里反复强调,权限数据不是前端随便写的。先在角色管理里给角色分配权限点,再把角色绑定给员工,登录后接口返回当前用户的 permissions。这个数据要供菜单过滤和按钮权限共同使用。
怎么想:getProfile 拿到的 profile 里包含 permissions。左侧菜单渲染时,从 Vuex 里取筛选后的 menuList;按钮权限指令里,从 store.state.user.profile.permissions 取当前用户权限列表,再用按钮自己的权限标识去匹配。也就是说,Vuex 是“接口数据”和“页面消费”之间的中转站。
怎么做:登录后触发 user/getProfile,把返回数据 commit 到 setProfile。按钮指令中先引入 store,再取 store.state.user.profile.permissions,用 includes 判断当前按钮标识是否存在。
易错点:一级路由权限和按钮权限不是一回事。转写稿中“楼宇、区域”这类一级权限用于控制左侧菜单;下面的“添加、删除”才是按钮权限。匹配按钮时不要拿错层级。
调试:切换用户后为什么还是旧菜单
为什么需要:转写稿演示了一个 bug:从低权限用户切到管理员后,左侧菜单仍然只有两项。原因是切换用户时复用了上一次的路由实例和 Vuex 缓存。
怎么想:只重新登录不够,旧路由记录还在;Vuex 里的 menuList 和 profile 也可能还是上一个用户的。要清两处:路由系统中通过 addRoute 添加的动态记录,以及 Vuex 中保存的菜单和用户信息。
怎么做:项目里封装了 resetRouter。转写稿说明它不是官方方法,而是用新 router 实例的 matcher 覆盖老实例的 matcher,从而清空之前 addRoute 添加的记录。退出登录时调用它,同时在 Vuex 中把 menuList 恢复为静态路由,把 user.profile 清空为空对象。这样新用户进来会重新走添加逻辑。
易错点:只清路由不清 Vuex,左侧菜单可能还读旧数据;只清 Vuex 不清路由,路由匹配仍可能复用旧记录。清空 profile 时保持结构,例如赋值为 {},避免消费处直接报错。
按钮权限中如何消费 Vuex
为什么需要:页面上很多按钮都要按权限显示或隐藏,不能每个按钮写一套判断。转写稿采用全局指令方案:按钮标识能在用户 permissions 里找到就显示,找不到就移除。
怎么想:指令的 inserted 钩子能拿到 el 和 binding.value。el 是按钮真实元素,binding.value 是按钮权限标识。再从 Vuex 取当前用户权限列表做 includes 判断。
怎么做:注册全局指令后,在按钮上写类似 v-btn-auth="'enterprise:add'"。指令内部取 store.state.user.profile.permissions,若 !permissions.includes(binding.value),就 el.remove()。转写稿中调试时先打印 el 和 binding.value,确认参数能拿到,再补核心逻辑。
易错点:指令写完要引入注册,否则模板会报“指令没有注册”;binding.value 要传真实权限标识,不能随便传字符串;permissions 初始可能为空,消费时要保证结构存在。转写稿还提到 render 函数和 H 函数,那是模板编写的拓展内容,本节 Vuex 逻辑的重点仍是:接口数据进 store,菜单和按钮从 store 取,退出登录时把 store 与路由缓存一起清干净。
先理解权限数据的来源:角色绑定员工
为什么需要:路由守卫里要触发 getProfile,前提是后端已经能返回当前用户的权限数据。转写稿里明确讲了,权限数据不是凭空写死的,而是先在“角色管理”里给角色分配功能权限点,再到“员工管理”里把角色绑定给员工。这样员工登录后,后端才能把该角色下的权限点返回给前端。这个设计就是 RBAC(基于角色的权限控制):先给角色分配权限,再把角色分配给员工,员工就自动拥有角色下的权限点。
怎么想:不要把 permissions 当成前端临时拼出来的数组。它来自后端接口,前端在登录后通过 getProfile 取回,并写入 Vuex。后续动态路由过滤、左侧菜单渲染、按钮权限判断,都依赖这份数据。所以本节的重点不是“写死权限”,而是“在路由守卫里触发获取权限的 action”。
怎么做:在角色管理里勾选权限点,例如园区管理下的楼宇管理、企业管理下的添加/编辑等;再到员工管理里把角色绑定给员工。切换成该员工账号登录后,刷新页面,在接口返回里查看 permission/permissions 字段。转写稿中调试时就是打开检查面板,找到当前用户数据,确认里面出现了刚才配置的权限点。
易错点:不要把“角色”和“员工”混为一谈;角色改权限后,员工绑定关系还在,但需要重新登录或重新获取用户信息才能拿到新权限;后端返回字段可能是 permission 或 permissions,要以实际接口为准,不能凭感觉写。
在路由守卫中触发 getProfile action
为什么需要:页面一跳转,路由守卫就会执行。此时如果只有 token,却还没有当前用户的权限数据,就无法判断动态路由和菜单。所以需要在 permission 文件的路由守卫里触发 user/getProfile,把权限数据取回来。
怎么想:先判断 token。有 token 时,先放行当前导航,再检查 store.state.user.profile.id 是否存在;如果没有,就说明用户信息还没取过,调用 store.dispatch('user/getProfile')。用 profile.id 做判断,是为了避免每次路由跳转都重复请求用户信息。
怎么做:保留本节笔记代码:
javascript
// 权限控制
import router from './router'
import { getCookie } from './utils/auth'
import store from './store'
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) {
// 获取原始权限列表
const permissions = await store.dispatch('user/getProfile')
console.log(permissions)
}
} else {
// 没有token
if (WHITE_LIST.includes(to.path)) {
next()
} else {
next('/login')
}
}
})易错点:next() 的调用时机要清楚。笔记代码是“有 token 先 next(),再 await 获取用户信息”,它表示先放行导航,再补用户信息;如果你希望“拿到权限后再放行”,就要把 next() 放到 await 之后,但要注意不能重复调用 next()。另外 WHITE_LIST.includes(to.path) 判断的是路径,不要写成 from.path;cookie 名要和登录时写入的一致,转写稿里是 park_token。
getProfile 里写入 store 并返回 permissions
为什么需要:路由守卫只负责触发 action,真正发请求、写 store、返回权限列表的是 getProfile。如果这里不 commit,后续菜单和按钮权限就拿不到数据;如果不 return,守卫里 await 得到的就是 undefined,无法打印和继续使用权限列表。
怎么想:getProfileAPI() 返回的 res.data 是用户信息,里面包含 permissions。先把整个用户信息写入 setProfile,再把 res.data.permissions 返回给调用方。这样 store 里有完整用户信息,守卫里也能直接拿到权限数组。
怎么做:保留本节笔记代码:
javascript
async getProfile(ctx) {
const res = await getProfileAPI()
ctx.commit('setProfile', res.data)
return res.data.permissions
}易错点:ctx.commit('setProfile', res.data) 的模块命名空间要确认,如果 user 模块开启了 namespaced: true,action 里通常写 commit('setProfile', ...) 是可以的,但外部 dispatch 要写 user/getProfile;不要把 res.data.permissions 写成 res.permissions;返回值不要漏写 return。
调试与验证:看请求、看 store、看打印
为什么需要:权限流程链路长,光看代码容易漏。转写稿里反复强调“每一步都看一下数据”,因为 permissions 是否返回、profile.id 是否写入、守卫是否重复触发,都会影响结果。
怎么想:先看接口返回有没有权限点;再看 store.state.user.profile 是否被 setProfile 写入;最后看守卫里的 console.log(permissions) 是否打印出数组。转写稿中就是通过检查面板确认 permission 字段里出现了刚才配置的两个权限点,再回到代码里确认 action 被触发。
怎么做:登录后刷新页面,打开控制台看 permissions;在 Vuex 里看 user/profile;如果发现左侧菜单不对,先确认当前账号的角色和员工绑定是否正确。转写稿里还提到切换用户后左侧仍显示上一个用户的菜单,这是路由缓存问题,后续会用 resetRouter 和清空 menuList 解决;但本节先确保 getProfile 能正确触发并返回权限数据。
易错点:不要只打印 res 不看 res.data;不要在 profile.id 已有值时还重复请求;刷新后如果 profile.id 丢失,要检查 store 是否持久化或是否重新登录;console.log 调试完记得清理。
为什么要把动态路由表拆出来
为什么需要:静态路由是每个用户都能看到的,项目初始化时就该注册;动态路由要跟当前用户的权限走,不同角色看到的菜单数量不一样。转写稿里反复强调:静态路由“一上来就放到 routes 选项位置”,动态路由“个数不一致,需要权限控制”。如果全塞进 routes,低权限用户也会把不该有的路由注册进去。
怎么想:先按“是否需要权限控制”分两桶。静态路由留在 router/index.js 的 routes;动态路由单独放一个文件,用 export const asyncRoutes 导出,当成“权限过滤的数据源”。
怎么做:把下面这份动态路由表从主路由文件里拆出来,单独导出。主路由只保留静态路由;权限流程里再 import { asyncRoutes },拿到当前用户 permissions 后过滤,得到“最终路由表”。
易错点:拆出去后忘记导出/导入,菜单和路由都不生效;不要把动态路由又塞回 routes,否则拆分失去意义。
动态路由表的结构设计
为什么需要:动态路由表不是随便写几个 path,它要同时服务三件事:生成左侧菜单、注册路由、做权限匹配。所以每一项要带 permission、meta.title、meta.icon,一级还要用 Layout 做容器。
怎么想:一级路由代表业务模块,permission: 'park' 这类标识控制模块是否出现;children 里的二级路由用 permission: 'park:building' 控制子菜单和页面。meta.title 给菜单文字,meta.icon 给菜单图标,component 用 () => import(...) 懒加载。
怎么做:保留本节笔记代码:
javascript
import Layout from '@/layout'
// 1. 动态路由: 需要做权限控制 可以根据不同的权限 数量上的变化
// 2. 静态路由: 不需要做权限控制 每一个用户都可以看到 初始化的时候初始化一次
// 动态路由表
export const asyncRoutes = [
{
path: '/park',
component: Layout,
permission: 'park',
meta: { title: '园区管理', icon: 'el-icon-office-building' },
children: [{
path: 'building',
permission: 'park:building',
meta: { title: '楼宇管理' },
component: () => import('@/views/Park/Building/index')
},
{
path: 'enterprise',
permission: 'park:enterprise',
meta: { title: '企业管理' },
component: () => import('@/views/Park/Enterprise/index')
}]
},
{
path: '/parking',
component: Layout,
permission: 'parking',
meta: { title: '行车管理', icon: 'el-icon-guide' },
children: [{
path: 'area',
permission: 'parking:area',
component: () => import('@/views/Car/CarArea'),
meta: { title: '区域管理' }
}, {
path: 'card',
permission: 'parking:card',
component: () => import('@/views/Car/CarCard'),
meta: { title: '月卡管理' }
}, {
path: 'pay',
permission: 'parking:payment',
component: () => import('@/views/Car/CarPay'),
meta: { title: '停车缴费管理' }
},
{
path: 'rule',
permission: 'parking:rule',
component: () => import('@/views/Car/CarRule'),
meta: { title: '计费规则管理' }
}]
},
{
path: '/pole',
component: Layout,
permission: 'pole',
meta: { title: '一体杆管理', icon: 'el-icon-refrigerator' },
children: [{
path: 'info',
permission: 'pole:info',
component: () => import('@/views/Rod/RodManage'),
meta: { title: '一体杆管理' }
}, {
path: 'waring',
permission: 'pole:warning',
component: () => import('@/views/Rod/RodWarn'),
meta: { title: '告警记录' }
}]
},
{
path: '/sys',
component: Layout,
permission: 'sys',
meta: { title: '系统管理', icon: 'el-icon-setting' },
children: [{
path: 'role',
permission: 'sys:role',
component: () => import('@/views/System/Role/index'),
meta: { title: '角色管理' }
}, {
path: 'user',
permission: 'sys:user',
component: () => import('@/views/System/Employee/index'),
meta: { title: '员工管理' }
}]
}
]易错点:permission 要和后端返回的权限标识严格一致,比如 parking:payment 不要写成 parking:pay;父子 path 拼接关系要清楚;component 写成函数返回 import(),不要提前执行。
过滤、注册和菜单渲染的推导
为什么需要:转写稿里说,拿到当前用户权限数据后,“过滤的是动态路由表”。权限数据在 permissions 字段里,可能是 park、parking 以及二级标识;动态路由表只是数据源,不能直接全量用。
怎么想:先调接口拿 permissions,再用一级、二级权限标识去过滤 asyncRoutes,得到“符合要求的最终路由表”。这份最终路由表有两个用途:一是存到 Vuex 的 menuList,供 Layout 里的侧边栏遍历渲染;二是通过 router.addRoute 动态加入路由系统。
怎么做:在权限流程中导入 asyncRoutes,过滤后 addRoute,再 commit 到 menuList。转写稿里调试时打印最终路由表,发现它仍然是标准的 route 配置对象,里面有 path、component 等,只是数量变成了当前用户有权限的两项。
易错点:只渲染菜单不 addRoute,页面刷新或直接访问地址会失效;addRoute 重复添加会出问题;退出登录不清空,会复用上一个用户的路由缓存。转写稿里切换管理员后左侧仍只有两项,就是旧路由实例缓存导致的,解决思路是退出登录时重置路由并清空 menuList 和用户权限数据。
先看本节代码:初始化只挂静态路由
本节的核心不是“把权限做完”,而是先把路由拆清楚:项目初始化时,routes 里只放静态路由;需要按用户权限筛选的动态路由,不在这里直接挂载,而是在后续权限流程里再 addRoutes 进去。这样初始化阶段就不会把无权限的路由提前暴露出来。
javascript
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
// 俩种路由
export const routes = [
{
path: '/login',
component: () => import('@/views/Login/index'),
hidden: true
},
{
path: '/addCard',
component: () => import('@/views/Car/CarCard/add-card'),
hidden: true
},
{
path: '/addEnterprise',
component: () => import('@/views/Park/Enterprise/AddEnterprise'),
hidden: true
},
{
path: '/enterpriseDetail',
component: () => import('@/views/Park/Enterprise/EnterpriseDetail'),
hidden: true
},
{
path: '/addRole',
component: () => import('@/views/System/Role/AddRole'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/workbench'
},
// 只有一级路由
// 一级路由:负责把layout架子渲染出来
// 二级路由:path为空 会作为默认的二级路由一上来就渲染出来
{
path: '/workbench',
component: Layout,
children: [{
path: '',
component: () => import('@/views/Workbench/index'),
meta: { title: '工作台', icon: 'el-icon-data-board' }
}]
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
}
]
const createRouter = () => new Router({
// mode: 'history', // require service support
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: [...routes]
})
const router = createRouter()
// 重置路由方法
export function resetRouter() {
// 得到一个全新的router实例对象
const newRouter = createRouter()
// 使用新的路由记录覆盖掉老的路由记录
router.matcher = newRouter.matcher
}
export default router为什么初始化只处理静态路由表
为什么需要:转写稿里明确区分了“静态路由”和“动态路由”。静态路由是一上来就放到 routes 选项位置、项目初始化后就能用的路由,不需要权限控制;动态路由表个数不固定,需要按当前用户权限筛选。
怎么想:如果初始化就把所有路由都挂上,低权限用户也可能访问到不该出现的页面;所以初始化只处理登录、404、工作台、隐藏详情页等静态项。
怎么做:把静态路由集中到 routes,createRouter() 只传 routes: [...routes]。
易错点:不要把动态路由直接塞进这个 routes 数组;hidden: true 只是菜单隐藏标记,不等于权限控制。
createRouter 的作用与浅拷贝
为什么需要:resetRouter 要能重新生成一个干净实例,所以先把创建逻辑抽成 createRouter()。
怎么想:初始化时实例化 Router,传入静态路由;后续重置时再调用同一个工厂函数。
怎么做:const router = createRouter() 得到当前路由实例;routes: [...routes] 做一层数组浅拷贝。
易错点:浅拷贝不是深拷贝,路由对象本身仍被引用;不要以为 [...routes] 能替代权限过滤。
resetRouter 为什么要覆盖 matcher
为什么需要:转写稿里说,切换用户后左侧菜单仍是上一个用户的缓存,原因是复用了上一次的路由实例;动态添加过的路由记录还在。
怎么想:resetRouter 不是官方 API,而是项目里自己补的方案:创建一个全新 Router,再用新实例的 matcher 覆盖老实例的 matcher,从而清掉之前 addRoutes 添加的记录。
怎么做:const newRouter = createRouter(); router.matcher = newRouter.matcher。
易错点:转写稿里“measure”是同音误写,实际是 matcher;重置路由后还要清 Vuex 里的 menuList 和用户权限数据,否则菜单和按钮权限仍可能残留。
退出登录时的调试流程
为什么需要:只重置路由,左侧菜单仍可能来自 Vuex 缓存。
怎么想:退出登录意味着要切换用户,应把“路由记录、菜单数据、用户权限”一起复原。
怎么做:退出登录时调用 resetRouter(),把 menuList 恢复为静态路由初始值,并清空 userProfile;再登录新用户时重新走权限筛选和 addRoutes。
易错点:不要只清一处;转写稿演示过,切换管理员后左侧仍只有两项,就是旧缓存没清干净。初始化阶段只处理静态路由表,动态路由要等权限数据拿到后再添加。
代码示例
示例一:路由拆分 —— 初始化时只挂静态路由,并导出 resetRouter
js
// src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
// 1. 静态路由:不需要权限控制,每个用户都能看到,初始化时只初始化一次
// 2. 动态路由:需要权限控制,数量随权限变化,放在 asyncRoutes.js 里
export const routes = [
{
path: '/login',
component: () => import('@/views/Login/index'),
hidden: true
},
{
path: '/addCard',
component: () => import('@/views/Car/CarCard/add-card'),
hidden: true
},
{
path: '/addEnterprise',
component: () => import('@/views/Park/Enterprise/AddEnterprise'),
hidden: true
},
{
path: '/enterpriseDetail',
component: () => import('@/views/Park/Enterprise/EnterpriseDetail'),
hidden: true
},
{
path: '/addRole',
component: () => import('@/views/System/Role/AddRole'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/workbench'
},
// 只有一级路由:一级路由负责把 layout 架子渲染出来
// 二级路由 path 为空,会作为默认的二级路由一上来就渲染
{
path: '/workbench',
component: Layout,
children: [{
path: '',
component: () => import('@/views/Workbench/index'),
meta: { title: '工作台', icon: 'el-icon-data-board' }
}]
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
}
]
const createRouter = () => new Router({
mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: [...routes]
})
const router = createRouter()
// 重置路由方法(非官方 API,是脚手架作者 hack 出来的)
export function resetRouter() {
// 得到一个全新的 router 实例对象
const newRouter = createRouter()
// 使用新的路由记录覆盖掉老的路由记录
router.matcher = newRouter.matcher
}
export default router关键行与易错点:
routes: [...routes]这里必须展开复制一份。如果直接写routes,后续addRoute往实例里加动态路由时容易污染源数组,导致重复登录后路由翻倍。resetRouter不是 Vue Router 官方方法,原理是:新建一个干净实例,把它的matcher(内部维护的真实路由记录对象)赋值给老实例的matcher,一次性抹掉所有addRoute加进去的动态记录。- 易错点:
resetRouter必须export出去,在 store 里 import 调用,不要在业务页面里自己new Router()。 - 易错点:只重置
matcher不够,左侧菜单数据存在 Vuex 里,还要同时把菜单数据也复原成静态路由表,否则页面左侧还是旧菜单。
示例二:Vuex 存用户权限 + 全局前置守卫触发 action
js
// src/store/modules/user.js
import { getProfileAPI } from '@/apis/user'
export default {
namespaced: true,
state: () => {
return {
profile: {}
}
},
mutations: {
setProfile(state, profile) {
state.profile = profile
},
clearProfile(state) {
state.profile = {}
}
},
actions: {
async getProfile(ctx) {
const res = await getProfileAPI()
ctx.commit('setProfile', res.data)
// 把权限标识数组返回给调用方(守卫里要用它过滤动态路由)
return res.data.permissions
}
}
}js
// src/store/modules/menu.js
import { routes } from '@/router'
import { resetRouter } from '@/router'
export default {
namespaced: true,
state: () => {
return {
// 初始值就是静态路由表,每个用户都一样
menuList: [...routes]
}
},
mutations: {
setMenuList(state, list) {
state.menuList = list
},
// 退出登录时清空路由 + 菜单
resetMenu(state) {
resetRouter()
state.menuList = [...routes]
}
}
}js
// src/permission.js
// 权限控制
import router from './router'
import { getCookie } from './utils/auth'
import store from './store'
const WHITE_LIST = ['/login', '/404']
router.beforeEach(async (to, from, next) => {
const token = getCookie('park_token')
// 有 token
if (token) {
next()
// 获取用户信息(profile.id 有值说明已经取过,不用重复请求)
if (!store.state.user.profile.id) {
// 获取原始权限列表
const permissions = await store.dispatch('user/getProfile')
console.log(permissions)
}
} else {
// 没有 token
if (WHITE_LIST.includes(to.path)) {
next()
} else {
next('/login')
}
}
})js
// 退出登录处(Header 组件里)
async logout() {
// 1. 清空用户信息
this.$store.commit('user/clearProfile')
// 2. 清空动态路由记录 + 复原菜单数据
this.$store.commit('menu/resetMenu')
// 3. 跳回登录页
this.$router.push('/login')
}关键行与易错点:
namespaced: true之后,action 必须写全名'user/getProfile',mutation 必须写全名'user/setProfile',少写模块名会提示 "unknown action type"。- action 里
return res.data.permissions很关键:dispatch返回的是 action 的返回值,守卫里await store.dispatch(...)才能拿到权限数组,否则拿到的是undefined。 if (!store.state.user.profile.id)是「是否已获取过用户信息」的判据,不加这个判断每次路由跳转都会重新请求接口。- 易错点:
next()只能调用一次,且不要在next()之后又写next('/login')。 - 易错点:白名单里必须包含
/404,否则无 token 访问 404 页面会被反复重定向到登录页。 - 易错点:切换账号后菜单不刷新,原因是复用了上一次的 router 实例缓存,解决方式就是上面
resetMenu里调resetRouter()+ 复原menuList。
示例三:按钮权限控制 —— 全局指令 v-btn-auth
js
// src/directive/btnAuth.js
import store from '@/store'
export default {
// inserted:指令绑定的元素被插入到 DOM 时自动执行
inserted(el, binding) {
// 1. 当前按钮独有的权限标识(指令传进来的值)
const btnPermission = binding.value
// 2. 当前用户拥有的权限列表(存在 Vuex 的 user.profile.permissions 里)
const permissions = store.state.user.profile.permissions || []
// 3. 找不到就说明没权限,把按钮从 DOM 里移除
if (!permissions.includes(btnPermission)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
}js
// src/directive/index.js
import Vue from 'vue'
import btnAuth from './btnAuth'
// 注册全局指令:名称不带 v- 前缀,使用时才写 v-btn-auth
Vue.directive('btn-auth', btnAuth)js
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/directive' // 执行一次,全局指令才生效
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')vue
<!-- 业务页面中使用 -->
<template>
<div>
<!-- 有 park:enterprise:add 权限才显示 -->
<el-button v-btn-auth="'park:enterprise:add'">添加企业</el-button>
<!-- 没有 park:enterprise:remove 权限,会被 removeChild 干掉 -->
<el-button v-btn-auth="'park:enterprise:remove'">删除</el-button>
</div>
</template>关键行与易错点:
Vue.directive('btn-auth', {...})第一个参数不带v-,模板里用时才写v-btn-auth。inserted是自动执行的钩子,不需要手动调用;此时元素已经插入 DOM,el.parentNode才有值,才能removeChild。binding.value拿到的就是等号后面引号里的内容。模板里必须写成v-btn-auth="'park:enterprise:add'"(内层再套一层单引号),如果写成v-btn-auth="park:enterprise:add",Vue 会把它当成变量去 data 里找,报错。- 易错点:
store.state.user.profile.permissions要加|| []兜底,否则用户信息还没请求回来时.includes会报 "Cannot read property 'includes' of undefined"。 - 易错点:
@/directive只在main.jsimport 一次即可,不 import 的话模板会报 "Failed to resolve directive: btn-auth"。 - 易错点:每个按钮都要配一个和后端约定好的独立权限标识(如
park:enterprise:add、park:enterprise:remove),标识必须和角色管理里勾选的权限点保持一致,否则永远匹配不上、按钮永远不显示。
常见错误
切换账号后左侧菜单仍是上一个用户的权限
现象:管理员登录后仍只显示低权限菜单。
原因:退出登录未重置路由记录,且 Vuex 中menuList/profile仍复用旧缓存。
修法:退出时统一调用resetRouter(),清空menuList和user/profile,再跳转登录页。动态路由已添加,但侧边栏不显示对应菜单
现象:地址栏可直接访问页面,左侧菜单缺失。
原因:只执行了动态添加路由,未把筛选后的路由存入menuList供侧边栏渲染。
修法:筛选后同时执行addRoutes与commit保存menuList。刷新页面后动态路由页面 404 或空白
现象:首次登录正常,刷新后动态页面丢失。
原因:守卫中先next()放行,异步获取权限和添加路由发生在放行之后。
修法:无profile.id时先await dispatch('user/getProfile')、过滤并添加路由,再next({ ...to, replace: true })。按钮权限指令不生效或控制台报“指令未注册”
现象:v-btn-auth无反应,按钮仍显示。
原因:指令文件未在入口导入执行,或指令名与模板中v-后名称不一致。
修法:在main.js中导入并执行注册;确认注册名如btnAuth对应v-btn-auth。按钮权限判断永远失败
现象:有权限的按钮也被移除。
原因:binding.value未传真实权限点,或permissions取值层级错误。
修法:模板传后端约定的权限标识;指令内从store.state.user.profile.permissions取值并用includes判断。重复添加动态路由导致重复菜单或警告
现象:多次进入页面后菜单重复、路由重复。
原因:每次守卫都执行addRoutes,未做一次性标记。
修法:用profile.id或独立标记判断是否已添加;退出登录时重置标记与路由。
练习
- 在退出登录逻辑中补齐清理流程:调用
resetRouter()、清空menuList、清空user/profile,说明为什么三者都要清。 - 改造
router.beforeEach:有 token 且无profile.id时,先拉取权限、过滤动态路由、添加路由、保存menuList,最后安全放行。 - 编写按钮权限指令核心逻辑:拿到
binding.value与当前用户permissions,无权限时从 DOM 移除元素。 - 用
render函数实现:根据level(1~6)生成h1~h6,文本为level对应值。 - 排查“切换管理员后仍显示低权限菜单”的 bug,列出至少 4 个检查点及修复动作。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 菜单权限控制复习 | 14:09 |
| 2 | 菜单权限控制-退出登录时清空路由 | 11:38 |
| 3 | 按钮权限控制-需求理解和指令基础架子搭建 | 15:18 |
| 4 | 按钮权限控制-指令核心逻辑编写 | 10:28 |
| 5 | 拓展render-render函数的基础语法 | 09:57 |
| 6 | 拓展render函数-适合render的场景 | 06:28 |
小结
- 权限数据来源于“角色分配权限点 + 员工绑定角色”,前端消费的是当前用户
permissions。 - 静态路由初始化注册;动态路由按权限过滤后添加,并同步保存给侧边栏渲染。
- 路由守卫中必须先完成权限获取和动态路由添加,再放行导航,避免刷新丢失。
- 退出登录或切换用户必须重置路由记录并清空
menuList/profile,否则会复用旧缓存。 - 按钮权限适合用全局指令统一处理,核心是权限标识匹配与 DOM 移除。
render函数适合需要 JS 编程灵活性生成结构的场景,常规页面仍优先template。- 排查权限问题按链路检查:接口
permissions→ 路由过滤结果 →addRoutes→menuList→ 按钮binding.value。