Skip to content
第 10 章 前端 ⏱ 13 分钟阅读

第 10 章:路由进阶 ​

学习目标 ​

  • 写路由守卫做权限拦截
  • 实现路由懒加载并预加载
  • 用 keep-alive 缓存页面
  • 处理路由跳转的 loading 状态

一、导航守卫 ​

ts
// 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大概率老教程,别照抄

二、组件内守卫 ​

vue
<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 拿不到
})

三、路由懒加载 + 预加载 ​

ts
const routes = [
  { path: '/about', component: () => import('@/views/About.vue') },
]
ts
// 首屏空闲时预加载
router.onError(() => console.error('路由加载失败'))
router.isReady().then(() => {
  // 首屏加载完后
})

Webpack/vite 都有 /* webpackPrefetch: true */ 提示:

ts
component: () => import(/* webpackPrefetch: true */ '@/views/About.vue')

💡 用 Vite 啥都不写,用 Webpack 才要手写 /* webpackPrefetch: true */——你项目是 Vite,所以不用管这注释。

四、keep-alive 缓存路由 ​

vue
<!-- App.vue -->
<template>
  <router-view v-slot="{ Component }">
    <keep-alive :include="['HomePage', 'UserPage']">
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>

缓存后组件不会销毁,onMounted 只跑一次,onActivated / onDeactivated 会触发。

五、路由过渡动画 ​

vue
<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>

六、滚动行为 ​

ts
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 状态 ​

ts
// 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
}
vue
<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,顺序反了不生效。

八、错误处理 ​

ts
router.onError((err) => {
  console.error('路由加载失败', err)
  // 上报错误、跳转错误页
})

九、本章小结 ​

API用途
beforeEach全局前置守卫
onBeforeRouteLeave离开组件前
keep-alive缓存路由组件
scrollBehavior控制滚动位置
router.onError加载错误回退

动手练习 ​

  1. 登录拦截:未登录访问 /admin 跳 /login,带 redirect 参数
  2. 回退确认:编辑页离开时弹确认框
  3. 滚动记忆:列表页回到上次位置

下一章:第 11 章:Pinia 状态管理 →

本站基于 VitePress 构建 · 由 StackHub 团队维护