第 10 章:路由进阶
学习目标
- 写路由守卫做权限拦截
- 实现路由懒加载并预加载
- 用
keep-alive缓存页面 - 处理路由跳转的 loading 状态
一、导航守卫
// src/router/index.ts
router.beforeEach((to, from) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
return { path: '/login', query: { redirect: to.fullPath } }
}
})
router.afterEach((to) => {
document.title = (to.meta.title as string) ?? '默认标题'
})⚠️ 坑 1:
beforeEach不 return = 放行,return 啥就执行啥(不是"一定要 return")。四种 return 值速查:
写法 效果 不写 return / return/return true/return undefined放行,继续跳转 return false中断,URL 不跳(啥也不干) return { path: '/login' }重定向到该路径 return '/login'重定向简写 一句话逻辑:不 return = 放行,return 啥就执行啥——不 return 不是"默认拦截",是"OK 你过去吧"。
💡
return是 Vue Router 4 新写法,next()是 Vue Router 3 老写法——本节示例用的是return,新代码别再用next()了。三种写法对照:
ts// ① 现代写法(Vue Router 4+ 推荐,本节用法) router.beforeEach((to, from) => { if (!token) return { path: '/login' } }) // ② 老写法(Vue Router 3 / 老项目)——有 next 参数,必须调一次 router.beforeEach((to, from, next) => { if (!token) next('/login') else next() // 不调 next 会卡死 }) // ③ ⚠️ 千万别混写 router.beforeEach((to, from, next) => { if (...) { next('/login') // 调了 return { path: '/login' } // 又 return → 报错 "next() called multiple times" } })啥时候还能见到
next:
场景 写法 新项目(Vue 3 + Router 4+) return老项目(Vue 2 + Router 3) next()(老代码别动)看到教程用 next大概率老教程,别照抄
二、组件内守卫
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges) {
return confirm('未保存,确定离开?')
}
})
onBeforeRouteUpdate((to, from) => {
// 同一组件,param 变化
console.log('路由参数更新', to.params.id)
})
</script>💡
Leave= 要走了,组件会被卸;Update= 还在,只是 url 参数变了。/user/1 → /user/3 ↓ 还是 UserDetail.vue,但 params.id 从 1 变 3 ↓ 不卸载、不重 mounted → 只触发 onBeforeRouteUpdate 组件自己 watch id 变化,重新拉数据 /user/1 → /order/5 ↓ 组件从 UserDetail 换成 OrderDetail ↓ UserDetail 触发 onBeforeRouteLeave OrderDetail 走正常 mounted 流程经典场景对照:
API 适用 onBeforeRouteLeave表单未保存提醒 / 关闭 socket / 清定时器 onBeforeRouteUpdate同组件换 id,重新拉详情( /user/1→/user/2)三个组件内守卫完整对照:
ts// ① 离开前(确认) onBeforeRouteLeave((to, from) => { if (hasUnsaved) return false // 阻止离开 }) // ② 同组件更新(参数变了) onBeforeRouteUpdate((to, from) => { fetchData(to.params.id) // 重新拉详情 }) // ③ 进入前(很少用,beforeEach 更常用) onBeforeRouteEnter((to, from) => { // 组件还没创建,this 拿不到 })
三、路由懒加载 + 预加载
const routes = [
{ path: '/about', component: () => import('@/views/About.vue') },
]// 首屏空闲时预加载
router.onError(() => console.error('路由加载失败'))
router.isReady().then(() => {
// 首屏加载完后
})Webpack/vite 都有 /* webpackPrefetch: true */ 提示:
component: () => import(/* webpackPrefetch: true */ '@/views/About.vue')💡 用 Vite 啥都不写,用 Webpack 才要手写
/* webpackPrefetch: true */——你项目是 Vite,所以不用管这注释。
四、keep-alive 缓存路由
<!-- App.vue -->
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="['HomePage', 'UserPage']">
<component :is="Component" />
</keep-alive>
</router-view>
</template>缓存后组件不会销毁,onMounted 只跑一次,onActivated / onDeactivated 会触发。
五、路由过渡动画
<template>
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in">
<component :is="Component" :key="route.fullPath" />
</transition>
</router-view>
</template>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>六、滚动行为
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition // 浏览器前进/后退
if (to.hash) return { el: to.hash } // 锚点
return { top: 0 } // 顶部
},
})💡
scrollBehavior= 控制路由切换后页面滚到哪——三个if分别管"浏览器前进/后退 / 锚点 / 默认滚顶"。不写的话 SPA 切页永远不动滚动条,体验很怪。三个 if 触发的实际场景对照:
用户操作 走的分支 滚动结果 /home滚到中间 → 点<router-link>去/about③ return { top: 0 }(普通跳转)/about滚到顶 ✓/home滚到中间 → 去/about→ 点浏览器 ← 后退① return savedPosition(浏览器后退,Vue 自动存了位置)/home滚回中间 ✓点击页面里 <a href="/doc#section3">跳过去② return { el: '#section3' }(URL 带 # 锚点)滚到 #section3元素 ✓直接在地址栏输入 /about回车③ return { top: 0 }滚到顶 ✓ 按键盘 Alt+← / Alt+→前进后退① return savedPosition还原位置 ✓ 移动端有顶栏遮挡锚点 ② 的返回值加 top: 80滚到锚点但下移 80px 三个 if 优先级(用户从浏览器后退到
/home#section3时):savedPosition 有值? → 是 → return savedPosition(优先还原历史位置,不滚到 #section3) ↓ 否 to.hash 有值? → 是 → return { el: to.hash } ↓ 否 return { top: 0 } → 滚顶关键限定:
savedPosition只在浏览器前进/后退时有值,普通<router-link>点击、地址栏输入、首次打开页面都是null。
七、Loading 状态
// src/composables/useRouteLoading.ts
import { ref } from 'vue'
import { useRouter } from 'vue-router'
export function useRouteLoading() {
const loading = ref(false)
const router = useRouter()
router.beforeEach((to, from) => {
if (to.path !== from.path) loading.value = true
})
router.afterEach(() => {
setTimeout(() => (loading.value = false), 100)
})
return loading
}<script setup lang="ts">
import { useRouteLoading } from '@/composables/useRouteLoading'
const loading = useRouteLoading()
</script>
<template>
<div v-if="loading" class="bar" />
<router-view />
</template>⚠️ 坑 2:
router.beforeEach会拦住所有路由,包括动态参数变化,要做去重。 ⚠️ 坑 3:transition配合keep-alive时,外层 transition 内层 keep-alive,顺序反了不生效。
八、错误处理
router.onError((err) => {
console.error('路由加载失败', err)
// 上报错误、跳转错误页
})九、本章小结
| API | 用途 |
|---|---|
beforeEach | 全局前置守卫 |
onBeforeRouteLeave | 离开组件前 |
keep-alive | 缓存路由组件 |
scrollBehavior | 控制滚动位置 |
router.onError | 加载错误回退 |
动手练习
- 登录拦截:未登录访问
/admin跳/login,带redirect参数 - 回退确认:编辑页离开时弹确认框
- 滚动记忆:列表页回到上次位置
下一章:第 11 章:Pinia 状态管理 →