提到 Vue Router,相信每一位前端开发者都不陌生。在日常开发中,尤其是搭建权限管控型后台管理系统时,很多同学应该都遇到过这样一个棘手的问题:我们需要通过前端实现权限菜单的动态渲染与路由控制。
先来看一段常见的业务代码:在路由前置守卫中,我们做了菜单路由的初始化处理 ------ 登录成功后,前端会请求接口获取当前用户对应的权限菜单列表,将其持久化存储后,在路由跳转前调用 initRouter 方法,实现动态路由的添加。
但此时会出现一个令人困惑的问题:当我们成功进入某个动态添加的路由页面(例如 /dashboard)后,一旦刷新页面,该路由就会 "丢失",页面无法正常渲染。这背后的原因究竟是什么呢?
js
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.path !== '/login' && !userStore.token) {
return next('/login')
}
if (!isInitRouter) {
console.log('初始化菜单Menu')
initRouter()
isInitRouter = true
}
return next()
})
js
// 一开始的默认静态路由
const router = createRouter({
history: createWebHistory(), // history模式
routes: [
{
path: '/',
name: 'layout',
redirect: '/dashboard',
children: [],
component: () => import('../views/layout/index.vue')
},
{
path: '/login',
component: () => import('../views/login/index.vue')
}
]
})
快照拍的是 "启动瞬间路由表的全貌",默认是静态路由表(因为动态路由还没机会注册),但如果动态路由提前注册了,快照就会包含它 。快照是「导航周期启动瞬间」拍的,而是「导航周期启动瞬间」拍一次,整个导航周期(包括所有守卫、匹配、渲染环节)都共用这份快照。
先拍快照,后初始化动态路由,初始化改不了已拍好的快照。
- 先拍快照 :导航周期一启动,就立刻给当前的路由表拍一张 "只读照片"(副本),这张照片里只有提前定义的静态路由(比如
/login),没有动态路由(比如/dashboard); - 后初始化 :快照拍好之后,才会进入
beforeEach守卫,执行你的initRouter()方法(动态添加/dashboard等路由,更新原路由表); - 初始化改不了已拍好的快照 :你通过
initRouter()确实更新了「原路由表」(新增了动态路由),但那张提前拍好的「快照照片」不会同步更新 ------ 它是一张独立的、固定不变的副本,导航周期全程只会用这张快照做事,不会再去读取更新后的原路由表。
next() 的调用方式 |
行为描述 | 是否开启新导航周期? |
|---|---|---|
next()(无参数) |
继续当前导航周期,用当前周期的快照匹配路由 | ❌ 不开启,沿用旧周期 |
next('/login')(带路径字符串) |
终止当前导航周期,发起一个新导航周期(目标:/login) |
✅ 开启新周期 |
next({ ...to, replace: true })(带路由对象) |
终止当前导航周期,发起一个新导航周期(目标:路由对象指定的路径) | ✅ 开启新周期 |
注:开启新的导航周期会重新走一次路由守卫
什么是导航周期?
导航周期的启动时机是刷新 / 进入页面的一瞬间」,但「导航周期本身是一整套连贯的流程」------ 不是 "瞬间结束",而是从 "启动瞬间" 开始,依次执行 "拍快照、守卫、匹配、渲染" 等步骤,直到页面显示完成才结束(只是整个流程很快,体感上像 "一瞬间")。
解决方案
一、要解决动态路由生效的问题,核心思路就是在添加路由后,通过 next(retryPath)等方式主动开启一个新的导航周期。在新的周期里,路由器就会基于包含新路由的、更新后的映射表来拍"快照"了
二、在跳转之前初始化路由表。正确的顺序是:先动态添加路由(让路由表变成最新),再执行跳转(启动新导航周期,拍新快照),新快照会包含最新路由表,跳转必然能找到对应路由,不会白屏。
快照触发时机
每次有效跳转都会启动一个新的导航周期------ 快照和 "导航周期" 是「一一对应」的:一个新导航周期,必然对应一次新快照;没有新导航周期,就不会有新快照。导航周期开始->新的快照->跳转
有效跳转:必然启动新导航周期,拍新快照
只要操作能引发「路由路径变化」或「路由查询参数 / 哈希值变化」(即路由状态改变),都属于有效跳转,一定会启动新导航周期,进而拍摄新快照。
- 路径完全变化(最常见)
-
- 示例:
/login→/dashboard、/dashboard→/profile、页面刷新/dashboard、点击<router-link to="/setting">、浏览器前进 / 后退(路径变化); - 结果:启动新导航周期,拍新快照(快照为当前路由表全貌)。
- 示例:
- 路径不变,查询参数变化
-
- 示例:
/list?page=1→/list?page=2(路径都是/list,仅查询参数page变化)、/detail?id=1→/detail?id=2; - 结果:同样启动新导航周期,拍新快照(哪怕路径不变,查询参数变化也属于路由状态变化,会触发新周期)。
- 示例:
- 路径不变,哈希值变化
-
- 示例:
/home#top→/home#bottom(路径/home不变,仅哈希值#后面的内容变化); - 结果:启动新导航周期,拍新快照。
- 示例:
无效跳转:不启动新导航周期,不拍新快照
只有一种情况属于无效跳转:跳转的目标路由与当前路由完全一致(路径、查询参数、哈希值均无变化) ,此时 Vue Router 会直接忽略该跳转请求,不会启动新导航周期,自然也不会拍摄新快照。
- 当前路由是
/dashboard,执行router.push('/dashboard')(路径、参数、哈希均一致); - 当前路由是
/list?page=1,执行router.push('/list?page=1')(查询参数无变化); - 当前路由是
/home#top,点击<router-link to="/home#top">(哈希值无变化)。
注意
js
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.path !== '/login' && !userStore.token) {
return next('/login')
}
if (!isInitRouter) {
console.log('初始化菜单Menu')
initRouter()
isInitRouter = true
}
return next({ ...to }) // 这样写会循环卡死
})
原因:触发了无限循环的导航周期------return next({ ...to }) 会持续终止当前导航周期,同时启动一个和当前目标完全一致的新导航周期,而新周期又会重复执行守卫逻辑,再次触发 return next({ ...to }),如此往复没有尽头,最终导致浏览器主线程被占用,页面卡死。
解决方案:仅在动态路由初始化后,按需执行 next({ ...to });其他场景执行 next(),同时添加 replace: true 优化体验。
步骤演示
一、正常进入(从 /login 跳转 /dashboard):没问题的流程
正常进入是「先访问 /login,登录后再跳转 /dashboard」,两步走,初始化时机提前了:
步骤 1:访问 /login(第一个导航周期,提前完成初始化)
-
触发导航:用户输入网址访问
/login→ 启动「导航周期 A」; -
拍快照 A:启动瞬间拍快照,此时路由表只有静态路由
/login(快照 A = 静态路由表); -
执行
beforeEach守卫:- 登录校验:
to.path === '/login',条件不成立,跳过; !isInitRouter === true→ 执行initRouter()(注册/dashboard等动态路由,路由表更新为「静态 + 动态」);isInitRouter设为true;return next()→ 继续导航周期 A,用快照 A 匹配/login,匹配成功 → 显示/login页面;
- 登录校验:
-
关键结果:此时「路由表已经更新」(有
/dashboard),只是导航周期 A 的快照 A 是旧的,但不影响/login显示。
步骤 2:登录成功,跳转 /dashboard(第二个导航周期,快照拍到新路由表)
-
触发导航:登录成功后执行
router.push('/dashboard')→ 启动「导航周期 B」; -
拍快照 B:启动瞬间拍快照,此时路由表已经是「静态 + 动态」(步骤 1 已初始化),快照 B = 完整路由表(含
/dashboard) ; -
执行
beforeEach守卫:- 登录校验:
to.path === '/dashboard'且有token,跳过; !isInitRouter === false→ 跳过initRouter();return next()→ 继续导航周期 B,用快照 B 匹配/dashboard,匹配成功 → 正常显示页面;
- 登录校验:
正常进入的核心:初始化提前完成
initRouter() 在第一个导航周期(/login)就执行了,路由表提前更新;后续跳转 /dashboard 时,新导航周期的快照能拍到完整路由表,自然没问题。
二、刷新 /dashboard:不行的流程
刷新是「直接访问 /dashboard」,一步到位,初始化时机滞后了:
步骤 1:刷新 /dashboard(唯一导航周期,先拍快照后初始化)
-
触发导航:用户刷新
/dashboard网址 → 启动「导航周期 C」; -
拍快照 C:启动瞬间拍快照,此时路由表还是「初始静态路由」(无
/dashboard,initRouter()还没执行),快照 C = 旧静态路由表; -
执行
beforeEach守卫:- 登录校验:
to.path === '/dashboard'且有token,跳过; !isInitRouter === true→ 执行initRouter()(注册/dashboard,路由表更新为「静态 + 动态」);isInitRouter设为true;return next()→ 继续导航周期 C,用快照 C(旧静态路由表)匹配/dashboard;
- 登录校验:
-
关键结果:快照 C 中没有
/dashboard,匹配失败 → 页面白屏(无组件可渲染)。
刷新不行的核心:顺序反了
「导航周期 C 启动(拍旧快照)」在前,「initRouter() 初始化(更新路由表)」在后;旧导航周期只能用已拍好的快照 C,哪怕后续路由表更新了,也无法改变快照 C 的内容,导致匹配失败。
扩展
vue-router@3 和 vue-router@4 路由守卫的return和next的区别是否触发请看下一篇文章《动态路由跳转失效?原来是 next() 与 return 的用法搞反了!》