主题
按钮权限与 Vue3 入门(二)
本页对应 按钮权限与 Vue3 入门,共 6 段视频,约 1 小时 15 分。
学习目标
- 能使用 render 函数获取插槽并实现基于按钮权限标识的高阶组件,完成有权限显示、无权限不渲染的验证。
- 能说明业务模块互相依赖、业务状态变化对显示与操作按钮的影响,并复述车辆入场/出场、告警派单等典型数据流转流程。
- 能概述 Vue3 在响应式底层 API、TypeScript 支持、组合式 API 和工具链方面的主要变化及学习必要性。
- 能使用 create-vue 创建 Vue3 项目,并按需选择配置项完成依赖安装与启动。
- 能识别 Vue3 项目的关键文件,说明 vite.config.js、package.json、main.js、App.vue 的作用及入口挂载流程。
- 能分别用选项式 API 和组合式 API 实现同一个自增案例,并对比二者在代码组织与开发思想上的异同。
知识点
本节要解决什么问题
本节笔记的核心是:项目初始化时,先把不依赖权限判断的静态路由表注册进 Vue Router。代码里 routes 里放着登录页、404、工作台、若干隐藏的新增/详情页,以及 Layout 布局路由。初始化只做一件事:用 createRouter() 生成 router 实例,把这份静态表装进去;后续动态权限路由如何注入,本节没有展开,不能凭空补。
转写稿主要讲按钮权限高阶组件、业务状态流转和 Vue3 入门,并不包含本节路由初始化的推导与调试。因此本节不把无关内容硬塞进来;只借用转写稿反复强调的验证习惯:改完先跑起来,用不同路径和刷新去验证,而不是只看代码。转写稿中“re函数、pros、VBS”等应分别理解为 render 函数、props、Vuex,但它们属于按钮权限方案,不是本节路由内容。
静态路由表:先把公共页面装进去
为什么需要:登录页、404、工作台、隐藏的新增页这些页面不依赖用户权限,项目一启动就应该能匹配到。如果等权限接口回来再注册,首屏和登录跳转容易空白或 404。
怎么想:把“所有用户都可能访问、与权限无关”的路由先抽成 routes 数组;需要权限的动态菜单路由后续再处理。本节标题说的“只处理静态路由表”,就是初始化阶段不碰动态权限路由。
怎么做:看代码中的 routes:/login、/addCard、/addEnterprise、/enterpriseDetail、/addRole 都设置 hidden: true,表示不在侧边菜单显示;/ 用 Layout 并 redirect: '/workbench';/workbench 的 children 里 path: '',作为默认二级路由;/404 也隐藏。
易错点:hidden: true 只影响菜单显示,不影响路由匹配;不要以为隐藏了就不能访问。children 的 path: '' 是默认子路由,访问父路径时会渲染它;写成 '/' 或漏写会导致默认页不出现。
布局路由与默认子路由
为什么需要:后台页面通常共用侧边栏、顶栏,Layout 负责把“架子”渲染出来;业务页面作为二级路由嵌进去。
怎么想:一级路由负责布局,二级路由负责内容。/workbench 用 Layout,子路由 path: '' 指向工作台页面,这样进入 /workbench 就能直接看到默认内容。
怎么做:代码中 /workbench 的 children 只有一项,path: '',component 动态导入工作台,meta 里放标题和图标。/ 则重定向到 /workbench。
易错点:redirect 和 children 默认路由不要混用错;/ 如果直接给 component: Layout 但没有默认子路由,会出现布局有了但内容空。动态导入路径要确保文件存在,否则运行时报模块找不到。
createRouter:生成实例并挂载静态表
为什么需要:Vue Router 需要先创建实例,才能被 Vue 使用;把静态表复制进去,避免后续重置时污染原始 routes。
怎么想:用工厂函数 createRouter() 每次返回新实例,内部 routes: [...routes] 展开复制一份静态表。这样初始化逻辑清晰,也给 resetRouter() 留出“重新生成”的入口。
怎么做:代码里 mode: 'history' 使用 history 模式,scrollBehavior: () => ({ y: 0 }) 让切换路由回到顶部,最后 export default router。
易错点:history 模式需要服务端支持,否则刷新子路径可能 404;如果只是本地练习,可切回 hash。scrollBehavior 返回 { y: 0 } 是常见写法,别写成 { x: 0 } 还误以为能回顶。
resetRouter:用新 matcher 覆盖旧记录
为什么需要:退出登录或切换权限后,之前动态加进去的路由可能残留;重置路由可以回到“只有静态表”的干净状态。
怎么想:不逐个删除路由,而是新建一个 router,拿到它的 matcher,覆盖当前 router 的 matcher。这样当前实例的路由匹配记录就换成了全新静态表。
怎么做:const newRouter = createRouter() 后执行 router.matcher = newRouter.matcher。注意这是依赖 Vue Router 内部属性的做法,版本升级时要验证。
易错点:重置后当前页面不会自动跳转,通常还要配合 router.push('/login') 等操作;不要直接改 routes 原数组,否则后续重置的“干净表”也被污染。
完整代码保留
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验证时按转写稿的调试习惯:启动项目后分别访问 /、/workbench、/login、/404,确认重定向、默认子路由和隐藏页匹配正常;再刷新 history 路径,检查服务端 fallback 是否配置。
本节目标:编写路由权限过滤函数
本节要完成的是一个“处理函数”:根据当前用户拥有的一级、二级权限标识,从完整的动态路由表 asyncRoutes 中过滤出真正可用的路由。转写稿里反复强调过一个判断思路:拿到权限标识后,去用户权限列表里用 includes 查找,找到才保留,找不到就不显示。本节把这个思路从“单个按钮是否显示”扩展到“整张路由表如何过滤”。
为什么需要:动态路由表往往是全量的,不能直接全部注册;如果用户没有某个一级模块或二级页面的权限,就不应该出现在路由结果里。
怎么想:先按一级权限过滤整组路由,再在保留下来的路由里过滤 children。
怎么做:用 filter 保留符合一级权限的路由,用 map 生成新对象并替换 children。
易错点:不要直接修改原数组;要确认 permission 字段和权限列表类型一致。
先看入参:三组数据分别是什么
javascript
// 根据权限标识过滤路由表
function getRoutes(firstRoutePerms, secondRoutePerms, asyncRoutes) {
// 根据一级和二级对动态路由表做过滤 return出去过滤之后的路由表
// 1. 根据一级路由对动态路由表做过滤
return asyncRoutes.filter(route => {
return firstRoutePerms.includes(route.permission)
}).map(item => {
// 2. 对二级路由做过滤
return {
...item,
children: item.children.filter(item => secondRoutePerms.includes(item.permission))
}
})
}为什么需要:函数必须明确“拿什么过滤什么”。firstRoutePerms 是一级权限标识集合,secondRoutePerms 是二级权限标识集合,asyncRoutes 是完整动态路由表。
怎么想:一级权限决定“整组路由要不要”,二级权限决定“组内子路由要不要”。
怎么做:一级用 filter,二级在 map 中处理 children。
易错点:权限集合应是数组;如果传进来的是字符串,includes 语义会变成“子串包含”,容易误判。
一级路由过滤:先缩小范围
为什么需要:如果一级模块都没有权限,后面的 children 再过滤也没有意义。
怎么想:沿用转写稿中的查找逻辑——当前路由的 permission 是否在 firstRoutePerms 中,找到就保留。
怎么做:
javascript
asyncRoutes.filter(route => {
return firstRoutePerms.includes(route.permission)
})filter 会返回一个新数组,不改变 asyncRoutes 本身。
易错点:route.permission 必须与权限列表中的值完全一致;大小写、前后空格、数字与字符串混用都可能导致过滤失败。
二级路由过滤:保留一级结构,只替换 children
为什么需要:一级路由保留后,组内可能仍有用户无权限的子页面,所以还要处理 children。
怎么想:不能直接在原对象上改 children,否则会污染原始动态路由表。应该用 map 返回新对象,并用展开运算符保留一级路由的其他字段。
怎么做:
javascript
.map(item => {
return {
...item,
children: item.children.filter(item => secondRoutePerms.includes(item.permission))
}
})这里的 ...item 复制一级路由字段,children 被替换成过滤后的新数组。
易错点:内层 filter 的参数也叫 item,会遮蔽外层 item,阅读时容易混淆;实际项目里可改成 child。另外,如果某个一级路由没有 children,直接调用 item.children.filter 会报错,需要先判断或兜底。
完整执行流程与验证思路
这个函数先执行一级 filter,得到“有权限的一级路由数组”;再执行 map,对每个一级路由生成新对象,并把 children 过滤成“有权限的二级路由数组”。最终返回的是过滤后的新路由表。
转写稿中验证按钮权限时,用过“换一个没有的标识,按钮就不显示”的方式。这里也可以同理验证:从 firstRoutePerms 去掉某个一级权限,观察整组路由是否消失;从 secondRoutePerms 去掉某个二级权限,观察对应子路由是否被过滤。调试时重点检查 permission 字段是否存在、权限数组是否为空、children 是否为数组。
易错点总结:不要省略 return;不要误用 map 替代 filter;不要直接修改 asyncRoutes;对 children 缺失要做兜底;权限标识类型和值必须严格匹配。
先看目标:动态路由为什么不能静态写死
不同角色登录后能看到的菜单不同,所以不能把所有路由都静态注册给所有人。更稳的做法是:保留一份“原始动态路由表”asyncRoutes,登录后拿到用户权限,再过滤出当前用户可用的路由。
为什么需要:菜单和页面访问权要跟用户权限走。
怎么想:原始路由表是“全集”,用户权限是“筛选条件”,最终得到“当前用户路由”。
怎么做:先把 asyncRoutes 导入守卫所在文件:
javascript
// 导入原始动态路由表
import { asyncRoutes } from './router/asyncRoute'易错点:过滤时不要直接修改 asyncRoutes 原数组,否则多次登录会互相污染;应让 getFilterRoutes 返回新数组。
守卫里的 token 分支:先判断身份再决定放行
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)
}
} else {
// 没有token
if (WHITE_LIST.includes(to.path)) {
next()
} else {
next('/login')
}
}
})为什么需要:有 token 才允许进入权限流程;无 token 只能去登录页或白名单。
怎么想:先看 token,再看白名单,最后才拉用户信息。
怎么做:无 token 时用 WHITE_LIST.includes(to.path) 判断;有 token 时进入权限加载。
易错点:next() 只能调用一次;原代码在 if (token) 里先 next(),后面又 await 拉权限,导航已经放行,过滤结果可能来不及注册。调试时会表现为“控制台有 filterRoutes,但刷新页面菜单/页面不对”。应把 next() 放到权限处理完成之后。
拉取用户信息:profile.id 作为“是否已加载”的标记
javascript
if (!store.state.user.profile.id) {
const permissions = await store.dispatch('user/getProfile')为什么需要:避免每次路由跳转都重复请求用户信息和权限。
怎么想:用 profile.id 是否存在判断“用户资料是否已加载”。
怎么做:await store.dispatch('user/getProfile'),把返回的权限数据交给下一步。
易错点:要确认 getProfile 返回的是权限列表,还是整个用户对象;如果返回结构是 { profile, permissions },就不能直接传给 getMenuList。另外 profile.id 初始值要稳定,否则会重复请求或死循环。
把权限数据转成菜单标识:getMenuList
javascript
const menuPerms = getMenuList(permissions)
console.log(menuPerms)为什么需要:后端返回的权限结构通常不能直接和路由 name/meta 对上。
怎么想:先抽出一级、二级菜单标识,形成路由过滤能识别的数组。
怎么做:getMenuList(permissions) 做数据整形,打印 menuPerms 验证字段。
易错点:空权限、null、字段名大小写、层级结构变化都会让过滤结果为空;先看 console.log(menuPerms),不要直接怀疑路由。
过滤一级和二级路由:getFilterRoutes
javascript
const filterRoutes = getFilterRoutes(menuPerms, asyncRoutes)
console.log(filterRoutes)为什么需要:只保留当前用户有权限访问的一级和二级路由。
怎么想:用 menuPerms 去筛选 asyncRoutes,父路由和子路由都要处理。
怎么做:先打印 filterRoutes,确认一级菜单、二级菜单是否都保留;确认没有权限的路由被剔除。
易错点:只过滤一级不过滤二级,会出现“菜单有但子页面进不去”;过滤后还要把结果真正注册到路由表,否则只是数组存在,路由并不存在。
关键调试:next() 的位置和动态注册
按本节笔记代码,next() 在拉权限之前执行,过滤结果只打印、未注册。实际项目里通常要改成:先 await 拉权限、过滤,再 router.addRoute 注册,最后再 next({ ...to, replace: true })。转写稿里按钮权限的调试思路是“先登录,再改权限标识验证显示/隐藏”,动态路由也可类比:先登录,再看 menuPerms、filterRoutes,最后验证刷新后页面是否可进。不要只盯控制台数组,要验证路由是否真的被加入。易错点:注册后再次进入守卫可能重复注册或死循环,需用 profile.id 或已注册标记控制;next 不要多次调用。
先看目标:这段 Vuex 逻辑在管什么
本节笔记的代码是一个 Vuex 模块,核心只做两件事:用常量路由初始化 menuList,再通过 mutation 把过滤后的动态路由追加进去。转写稿里多次把 Vuex 说成“VBS”“VS”“VX”,这里统一校正为 Vuex;把“拼年”校正为 Pinia。转写稿还提到“当前用户所有权限标识存放在 Vuex 里”,这说明 Vuex 在本项目里承担集中状态管理职责。但转写稿没有直接展开 setMenuList 的逐行调试,所以下面只依据笔记代码和转写稿中的状态管理背景做推导,不编造额外业务。
为什么需要:后台菜单通常由“常量路由”和“权限过滤后的动态路由”共同组成。常量路由是所有人都能看的,动态路由要登录后按权限筛出来。把最终菜单数据放进 Vuex,多个组件就能共享同一份菜单状态。
怎么想:先让 state 持有常量路由副本,等权限路由算出来后,再提交 mutation 合并。合并结果就是最终用于渲染菜单的 menuList。
怎么做:保留笔记代码原样:
javascript
import { contantsRoutes } from '@/router'
export default {
namespaced: true,
state: () => {
return {
menuList: [...contantsRoutes]
}
},
mutations: {
setMenuList(state, filterRoutes) {
state.menuList = [...state.menuList, ...filterRoutes]
}
}
}易错点:contantsRoutes 这个导入名必须和 @/router 里实际导出的名字完全一致。它看起来像 constantsRoutes 的拼写变体,但不要凭感觉改代码;如果导出名是 constantRoutes 而这里写 contantsRoutes,运行时就是 undefined,展开运算符会直接报错。
state 为什么写成函数
为什么需要:Vuex 的 state 如果写成对象,模块被复用时可能共享同一份引用,造成状态污染。写成函数,每次初始化都能返回新对象。
怎么想:menuList: [...contantsRoutes] 不是直接引用常量路由数组,而是用扩展运算符复制一层。这样后续追加动态路由时,不会直接改动路由配置文件里的原数组。
怎么做:保持 state: () => ({ menuList: [...contantsRoutes] }) 的函数形式;如果常量路由里还有嵌套 children,要注意这只是浅拷贝,深层对象仍是引用。
易错点:不要写成 state: { menuList: contantsRoutes },也不要在 mutation 里直接 state.menuList.push(...filterRoutes) 后以为什么都没问题。笔记采用“重新赋值新数组”的写法,更符合不可变更新习惯。
mutation 的合并逻辑与调用方式
为什么需要:Vuex 要求修改 state 走 mutation,便于追踪状态变化。setMenuList 的职责就是把权限过滤后的 filterRoutes 合并进 menuList。
怎么想:state.menuList = [...state.menuList, ...filterRoutes] 表示“旧菜单 + 新过滤路由”。这里默认 filterRoutes 是数组;如果不是数组,展开会出问题。
怎么做:组件里提交时,因为模块设置了 namespaced: true,要带命名空间,例如 commit('模块名/setMenuList', filterRoutes)。调试时可以先打印 filterRoutes 是否为数组,再提交后查看 state.menuList.length 是否增加。
易错点:第一,namespaced: true 后不能只写 commit('setMenuList');第二,重复提交会重复追加,若页面刷新或多次登录,需要先重置为常量路由再合并;第三,mutation 只做同步操作,异步请求权限路由应在 action 或组件中完成后再提交。
和转写稿的对应关系
转写稿讲按钮权限时反复出现“查找逻辑”:拿当前按钮标识去用户权限列表里找,找到才显示,找不到不返回。这个思路和菜单权限过滤是同一类思想:先有权限数据,再做条件筛选,最后把结果交给视图。转写稿还提到“Vuex 是集中状态管理工具”,而本节代码正是把菜单结果集中存起来。术语上注意:props 不是“pros”,render 不是“re函数”,Vuex 不是“VBS/VS/VX”。本节笔记没有附带图片,因此不插入图片;代码按原样保留。
先看目标:为什么要在路由守卫里触发 mutation
本节笔记的核心不是“写一个守卫”,而是把“用户权限 → 动态路由 → 左侧菜单”这条链路串起来,最后通过 store.commit('menu/setMenuList', filterRoutes) 触发 mutation,把过滤后的路由存进 Vuex,供菜单渲染使用。
为什么需要这样做?因为后台项目的菜单和路由不能写死:不同用户登录后,能看到的页面不同;如果只在前端静态写死菜单,权限控制就形同虚设。怎么想?先判断有没有 token,有 token 才继续拿用户信息;拿到权限后,生成菜单权限列表,再过滤动态路由,注册路由,最后提交 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')
}
}
})易错点:next() 的位置很关键。上面代码在有 token 后先 next(),再异步 dispatch、addRoute、commit,首次进入动态路由页面时可能放行过早,导致目标路由还没注册、菜单还没写入。实际调试时应确认:动态路由添加完成后再放行,或使用 next({ ...to, replace: true }) 重新进入,避免白屏或 404。
权限链路:getProfile → getMenuList → getFilterRoutes
为什么需要这三步?因为后端返回的“原始权限列表”不一定直接等于菜单结构,也不一定直接等于路由结构。怎么想?先把用户信息里的权限取出来,再转成菜单权限,最后用菜单权限去过滤 asyncRoutes,得到当前用户可访问的动态路由。怎么做?代码里用 store.dispatch('user/getProfile') 获取权限,赋值给 permissions;再用 getMenuList(permissions) 得到 menuPerms;最后用 getFilterRoutes(menuPerms, asyncRoutes) 得到 filterRoutes。这里的 getMenuList、getFilterRoutes 是项目已有工具函数,本节不展开内部实现。
转写稿中按钮权限的调试过程可以迁移理解:先从 Vuex 取当前用户所有权限标识,再用 includes 判断某个标识是否存在,找到才显示。菜单过滤同理:先有 menuPerms,再与 asyncRoutes 做匹配过滤。转写稿里“re函数”应纠正为 render 函数,“pros”应纠正为 props,但本节重点是权限列表的消费方式。
易错点:profile.id 不存在才请求用户信息,避免重复请求;但权限为空时不要直接崩溃,应确认 getMenuList 的入参结构。console.log(menuPerms) 和 console.log(filterRoutes) 是调试手段,上线前应清理或按规范保留。
mutation 的职责:存过滤后的路由
为什么需要 mutation?因为左侧菜单要响应式渲染,菜单数据应集中放在 Vuex 中,而不是散落在守卫局部变量里。怎么想?动态路由注册解决“能不能进页面”,mutation 存菜单解决“左侧显示什么”。怎么做?store.commit('menu/setMenuList', filterRoutes) 触发 menu 模块的 setMenuList,把过滤后的路由存入状态。注意这里是 commit,不是直接改 store.state.menu.menuList,保证状态变更可追踪。
易错点:模块命名要匹配,menu/setMenuList 中的 menu 是 Vuex 模块名;filterRoutes 应是可序列化、菜单组件能消费的结构;重复执行守卫时避免重复 addRoute 和重复提交,必要时做已添加标记。无 token 时走白名单判断,WHITE_LIST.includes(to.path) 命中才放行,否则 next('/login')。
代码示例
1. src/router/index.js —— 初始化时只处理静态路由表
javascript
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
// 俩种路由:静态路由(所有人可见)+ 动态路由(按权限过滤,见 asyncRoute.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
}
]
// 工厂函数:方便每次生成一个全新的 router 实例
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、工作台、各种hidden: true的详情/新增页。动态菜单页一律不放这里,否则会绕过权限。- 一级路由
component: Layout负责渲染出整个后台架子(侧边栏 + 顶栏 + 内容区);二级路由path: ''才是默认显示的子页面,写成'/'或漏掉空 path 都会导致首屏空白。 const createRouter = () => new Router({...})必须是工厂函数,resetRouter才能拿到一份全新的matcher去覆盖旧的;如果直接new Router()复用同一个实例,切换账号后旧动态路由仍残留。mode: 'history'需要服务端把 404 回退到index.html,否则刷新子路由白屏;本地调试不确定时先用默认 hash 模式。router.matcher = newRouter.matcher是 Vue Router 3 的重置写法,只重置路由记录,不会重置页面状态。
2. 动态路由权限完整流程:过滤函数 + 路由守卫 + Vuex 菜单
javascript
// ========== ① 根据权限标识过滤动态路由表 ==========
// firstRoutePerms / secondRoutePerms:一级、二级菜单的权限标识数组
// asyncRoutes:原始动态路由表(每项都带 permission 字段)
function getRoutes(firstRoutePerms, secondRoutePerms, asyncRoutes) {
// 1. 根据一级路由对动态路由表做过滤
return asyncRoutes
.filter(route => firstRoutePerms.includes(route.permission))
.map(item => {
// 2. 对二级路由做过滤
return {
...item,
children: item.children.filter(child => secondRoutePerms.includes(child.permission))
}
})
}
// ========== ② Vuex:左侧菜单数据源 ==========
// store/modules/menu.js
import { routes as contantsRoutes } from '@/router'
export default {
namespaced: true,
state: () => {
return {
menuList: [...contantsRoutes] // 初始只有静态路由(含 layout 架子)
}
},
mutations: {
setMenuList(state, filterRoutes) {
// 静态 + 动态,合并后才是完整菜单
state.menuList = [...state.menuList, ...filterRoutes]
}
}
}
// ========== ③ 路由守卫:拿权限 → 过滤 → addRoute → 存 Vuex ==========
import router from './router'
import store from './store'
import { getCookie } from '@/utils/auth'
import { asyncRoutes } from './router/asyncRoute'
const WHITE_LIST = ['/login']
router.beforeEach(async (to, from, next) => {
const token = getCookie('park_token')
// 有 token
if (token) {
next()
// 获取用户信息(用 profile.id 判断是否已拉过,避免每次跳转都请求)
if (!store.state.user.profile.id) {
// 1. 获取原始权限列表
const permissions = await store.dispatch('user/getProfile')
// 2. 数据处理得到一级二级路由菜单标识
const menuPerms = getMenuList(permissions)
// 3. 通过菜单权限过滤一级和二级,得到权限菜单列表
const filterRoutes = getFilterRoutes(menuPerms, asyncRoutes)
// 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')
}
}
})关键行与易错点
- 过滤要两级都过滤:先
filter一级,再map出{ ...item, children: item.children.filter(...) }。只过滤一级会把无权的子菜单全带出去,等于权限失效。 ...item必须展开保留原路由的component、meta、path,只替换children;直接返回{ children: ... }会丢掉component,页面渲染不出来。- 笔记里函数定义名是
getRoutes、调用处写的是getFilterRoutes、getRoutes(menuPerms, asyncRoutes)是两参、定义是三参——实际代码里名字和形参顺序必须完全一致,这是最容易照抄出 bug 的地方。 !store.state.user.profile.id这个判断是「只处理一次」的开关,配合store.commit('menu/setMenuList', ...)只 commit 一次,否则每次路由跳转都会重复追加菜单。next()写在addRoute之前,首次刷新进入动态页面时会先匹配一次「路由不存在」;生产上通常要改成动态路由加完后next({ ...to, replace: true })重新进入一次,否则首屏白屏/跳 404。- 退出登录时除了清 token,还要
resetRouter()并清空menuList,否则换账号会看到上一个账号的菜单。
3. Vue3 选项式 API 与组合式 API 对比(自增小案例)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>选项式 vs 组合式</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app1"></div>
<div id="app2"></div>
<script>
const { createApp, ref } = Vue
/* ---------- 写法一:选项式 API(Vue2 风格,Vue3 同样支持) ---------- */
const OptionsApp = {
data() {
// 第一步:定义响应数据
return { count: 0 }
},
methods: {
// 第二步:定义修改数据的方法
addCount() {
this.count++
}
},
template: `
<div>
<p>选项式:{{ count }}</p>
<button @click="addCount">加号</button>
</div>
`
}
/* ---------- 写法二:组合式 API(setup) ---------- */
const CompositionApp = {
setup() {
// 第一步:定义响应数据(ref 专门生成响应式数据,作用类似 data)
const count = ref(0)
// 第二步:定义修改响应式数据的方法
const addCount = () => {
count.value++ // ref 生成的数据必须通过 .value 修改
}
// 模板里要用到的东西,必须以对象形式 return 出去
return { count, addCount }
},
template: `
<div>
<p>组合式:{{ count }}</p>
<button @click="addCount">加号</button>
</div>
`
}
createApp(OptionsApp).mount('#app1')
createApp(CompositionApp).mount('#app2')
</script>
</body>
</html>关键行与易错点
- 两种写法开发思想完全一致:都是「先定义响应数据 → 再定义修改它的方法 → 最后在模板里绑定」,只是 API 换了;选项式在 Vue3 里照常能跑。
ref(0)生成的响应式数据在 JS 里必须写count.value++,直接count++不会触发更新(这是组合式最高频的坑);模板里 Vue 会自动解包,写{{ count }}即可,不要写{{ count.value }}。setup里声明的变量和方法必须 return,否则模板拿不到;返回的是对象,count对应模板里的count。setup里就是纯 JS 环境:局部变量 + 局部函数,写法和原生 JS 一样,不再「date 放数据、methods 放方法」各有各的位置。- 挂载用
createApp(根组件).mount('#app'),Vue3 没有new Vue();一个容器只能挂一个应用,所以示例里用了#app1、#app2两个容器。
常见错误
有 token 时先
next(),再异步获取用户信息并addRoute
现象:刷新动态路由页面 404/白屏,首次进入菜单不出现。
原因:守卫放行早于动态路由注册。
修法:先await完成getProfile → 过滤路由 → addRoute → commit 菜单,再next({ ...to, replace: true })。addRoute后直接next(),不重新匹配当前地址
现象:动态路由已添加,但当前页仍 404。
原因:本次导航已按旧路由表匹配。
修法:addRoute完成后用next({ ...to, replace: true })重新进入目标地址。404 兜底路由先于动态路由生效
现象:权限路由被 404 拦截。
原因:通配路由提前参与匹配。
修法:404 放在静态路由末尾或动态路由添加后再追加,确保业务动态路由先注册。退出登录只
resetRouter(),不重置 Vuex 菜单
现象:换账号后左侧菜单残留、按钮权限残留。
原因:router.matcher重置了,但menuList/profile/token未同步清理。
修法:登出时同时清 token、profile,commit将menuList重置为静态路由,再resetRouter()。setMenuList用追加方式且无幂等控制
现象:刷新或重复登录后菜单重复。
原因:mutation 每次[...state.menuList, ...filterRoutes]。
修法:改为“静态路由 + 本次过滤路由”整体替换,或增加hasAddRoutes标记防止重复提交。getRoutes对children直接filter
现象:无children的一级路由报错。
原因:未判空。
修法:使用children?.filter(...),无children时按业务规则保留或跳过。按钮权限高阶组件中直接返回
this.$slots.default
现象:按钮不显示、渲染报错或结构异常。
原因:默认插槽可能不存在,且$slots.default不是可直接返回的单 VNode。
修法:判空后取this.$slots.default?.(),返回合法 VNode/数组包裹;权限列表未加载时先不渲染或等待。Vue3 项目仍沿用 Vue2 写法
现象:全局组件不可用、启动报错、编辑器无高亮提示。
原因:仍用Vue.component/new Vue/Vetur,或setup中遗漏return、ref修改未.value。
修法:改用createApp().component().mount()、Volar,检查setup返回值与ref.value。
练习
阅读给定
router.beforeEach:指出“有 token 分支先next()”会导致什么具体问题,并改写为正确的异步守卫流程伪代码。设计“退出登录 → 切换账号 → 重新登录”的清理清单:说明
token、profile、menuList、resetRouter()、动态路由记录的清理顺序与原因。改造
getRoutes:要求兼容一级路由无children、二级路由无permission、重复提交导致菜单重复三种情况,并写出关键判断。用
render实现按钮权限组件:要求处理默认插槽为空、权限列表未加载、Vue3 全局注册三个边界场景,写出组件核心结构与注册方式。列一份 Vue3 项目迁移检查清单:至少覆盖 Vetur/Volar、入口文件、单文件组件、
setup返回值、ref使用五项,并说明每项检查点。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 通过render实现高阶组件方案 | 19:44 |
| 2 | 业务典型场景说明 | 15:28 |
| 3 | Vue3-简单介绍 | 06:26 |
| 4 | Vue3-创建一个Vue3的项目 | 07:40 |
| 5 | Vue3-项目关键文件熟悉 | 17:30 |
| 6 | Vue3-俩种API风格对比 | 08:30 |
小结
- 动态路由权限链路的核心顺序是:取用户信息 → 过滤路由 →
addRoute→ 存菜单 → 再放行。 next()的调用时机决定首屏是否 404,动态路由场景下不能先放行再注册。- 静态路由与动态路由必须分离,登录、404、工作台等基础路由不应被权限过滤误伤。
resetRouter()只解决路由匹配器问题,必须与 Vuex 菜单、用户信息、token 同步重置。- 菜单状态写入要幂等,避免刷新、重复登录、重复守卫造成菜单重复。
- 按钮权限高阶组件的本质是条件渲染,重点在插槽判空、权限数据就绪和合法 VNode 返回。
- Vue3 的组合式 API 是写法变化,不是开发思想推倒重来;选项式 API 仍可继续使用。
- Vue3 工具链要同步升级:Vite 替代旧构建体验,Volar 替代 Vetur,
createApp替代new Vue。