Skip to content

Vue Router 4.x

大白话解释: Vue Router 就像"页面导航员"。用户点击链接、输入 URL 时,Vue Router 决定显示哪个页面组件,不用刷新整个页面。

为什么需要 Vue Router?

  • 单页应用(SPA):一个页面内切换不同"页面"内容,不用刷新
  • URL 同步:浏览器地址栏显示正确的 URL,可以前进后退
  • 路由守卫:控制谁能访问哪些页面(登录验证、权限控制)
  • 懒加载:访问页面时才加载代码,加快首屏速度

Vue Router 4 vs Vue Router 3:

  • 创建方式createRouter() 替代 new VueRouter()
  • 历史模式createWebHistory() 替代 mode: 'history'
  • 守卫写法:不需要 next(),直接 return 路由对象或 false

Vue Router 4 是适配 Vue 3 的路由管理器。相比 Vue Router 3,API 更简洁,TypeScript 支持更好。


与 Vue Router 3 的主要差异

特性Vue Router 3Vue Router 4
创建方式new VueRouter()createRouter()
历史模式mode: 'history'createWebHistory()
通配路由path: '*'path: '/:pathMatch(.*)*'
守卫 next()必须调用可选,return 替代
安装方式Vue.use(VueRouter)app.use(router)
插件注册new Vue({ router })app.use(router)

基本配置

安装

bash
yarn add vue-router@4

创建路由

ts
// router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: { title: '首页' },
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { title: '登录' },
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true, title: '仪表盘' },
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('@/views/user/Layout.vue'),
    props: true, // 将 params 作为 props 传入
    children: [
      {
        path: '',
        name: 'UserProfile',
        component: () => import('@/views/user/Profile.vue'),
      },
      {
        path: 'posts',
        name: 'UserPosts',
        component: () => import('@/views/user/Posts.vue'),
      },
      {
        path: 'settings',
        name: 'UserSettings',
        component: () => import('@/views/user/Settings.vue'),
        meta: { requiresAuth: true },
      },
    ],
  },
  {
    // 通配路由 —— Vue Router 4 语法
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/404.vue'),
  },
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition
    if (to.hash) return { el: to.hash, behavior: 'smooth' }
    return { top: 0 }
  },
})

export default router

注册路由

ts
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

路由模式

ts
import { createWebHistory, createWebHashHistory, createMemoryHistory } from 'vue-router'

// history 模式 —— URL 干净
createWebHistory()        // 默认 BASE_URL
createWebHistory('/app/') // 自定义 base

// hash 模式 —— 兼容性好
createWebHashHistory()

// 内存模式 —— SSR / 测试
createMemoryHistory()

路由守卫

大白话解释: 路由守卫就像"进门前查身份证"。每次访问一个页面之前,先检查一下:

  • 你有没有登录?(没有就跳转登录页)
  • 你有没有权限访问这个页面?(没有就提示无权限)
  • 你要访问的页面需要什么特殊条件?

什么时候用路由守卫?

  • 登录验证:需要登录才能访问的页面,没登录就跳转登录页
  • 权限控制:管理员页面,普通用户不能访问
  • 页面标题:根据路由设置不同的页面标题
  • 进度条:页面切换时显示加载进度

全局前置守卫

ts
import router from './router'
import NProgress from 'nprogress'

router.beforeEach((to, from) => {
  NProgress.start()

  // 设置页面标题
  const title = to.meta.title  // 获取路由 meta 中的标题
  if (typeof title === 'string') {
    document.title = `${title} - My App`  // 设置页面标题
  }

  // 鉴权
  const token = localStorage.getItem('token')
  const requiresAuth = to.matched.some((record) => record.meta.requiresAuth)

  if (requiresAuth && !token) {
    // Vue Router 4 可以直接 return 路由对象(不需要 next)
    return { name: 'Login', query: { redirect: to.fullPath } }
  }

  if (to.name === 'Login' && token) {
    return { name: 'Dashboard' }
  }

  // 不 return 表示放行
})

💡 Vue Router 4 中 beforeEach 不再强制要求 next(),直接 return false 或路由对象即可。

全局后置钩子

ts
router.afterEach((to, from) => {
  NProgress.done()
})

路由独享守卫

ts
{
  path: '/admin',
  component: AdminLayout,
  beforeEnter: (to, from) => {
    const userRole = useUserStore().role
    if (userRole !== 'admin') {
      return { name: 'Forbidden' }
    }
  },
}

组件内守卫

vue
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// 路由参数变化时(组件复用)
onBeforeRouteUpdate((to, from) => {
  // 重新获取数据
  fetchUser(to.params.id as string)
})

// 离开当前路由前
onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const answer = window.confirm('有未保存的更改,确定离开吗?')
    if (!answer) return false // 阻止导航
  }
})
</script>

路由元信息 (meta)

ts
// 扩展 meta 类型
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    title?: string
    roles?: string[]
    keepAlive?: boolean
    breadcrumb?: boolean
  }
}
ts
const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    meta: {
      requiresAuth: true,
      title: '仪表盘',
      roles: ['admin', 'editor'],
      keepAlive: true,
    },
  },
]
ts
// 在守卫中使用
router.beforeEach((to) => {
  const requiredRoles = to.meta.roles
  if (requiredRoles) {
    const userRole = useUserStore().role
    if (!requiredRoles.includes(userRole)) {
      return { name: 'Forbidden' }
    }
  }
})

编程式导航

ts
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()

// 获取当前路由参数
console.log(route.params.id)
console.log(route.query.tab)
console.log(route.hash)
console.log(route.fullPath)

// 路由跳转
router.push('/home')
router.push({ name: 'User', params: { id: 1 } })
router.push({ path: '/search', query: { keyword: 'vue' } })
router.push({ path: '/about', hash: '#team' })

// 替换当前路由(不产生历史记录)
router.replace({ name: 'Login' })

// 前进后退
router.go(-1)
router.back()
router.forward()

导航结果处理

ts
import { isNavigationFailure, NavigationFailureType } from 'vue-router'  // 导入导航失败相关 API

// push 返回 Promise
const result = await router.push({ name: 'User', params: { id: 1 } })

// 检查导航是否被中断
router.push('/some-path').catch((failure) => {
  // failure 是 NavigationFailure 类型
  if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
    console.log('导航被守卫中断')
  }
})

导航守卫执行顺序

  1. 导航被触发
  2. 在失活的组件里调用 beforeRouteLeave
  3. 调用全局 beforeEach
  4. 在重用的组件里调用 beforeRouteUpdate
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局 beforeResolve
  9. 导航被确认
  10. 调用全局 afterEach
  11. 触发 DOM 更新

路由懒加载

ts
// 基本懒加载:访问时才加载组件
const Home = () => import('@/views/Home.vue')

// ⚠️ webpackChunkName 在 Vite 中无效,这是 Webpack 特有语法
// Vite 中使用以下方式分组(需要手动配置 rollupOptions)
// const UserList = () => import('@/views/user/List.vue')
// const UserDetail = () => import('@/views/user/Detail.vue')

// Vite 中自动 code splitting:相同目录的模块会自动合并
const Dashboard = () => import('@/views/Dashboard.vue')

命名视图

同一页面同时展示多个视图。

ts
{
  path: '/dashboard',
  components: {
    default: DashboardMain,
    sidebar: DashboardSidebar,
    header: DashboardHeader,
  },
}
vue
<template>
  <div class="layout">
    <router-view name="header" />
    <div class="content">
      <router-view name="sidebar" />
      <router-view />
    </div>
  </div>
</template>

动态路由

addRoute / removeRoute

运行时动态添加或移除路由,适合权限管理系统。

ts
import { useRouter } from 'vue-router'

const router = useRouter()

// 添加路由
router.addRoute({
  path: '/admin',
  name: 'Admin',
  component: () => import('@/views/Admin.vue'),
  meta: { requiresAdmin: true },
})

// 添加子路由
router.addRoute('Layout', {
  path: 'settings',
  name: 'Settings',
  component: () => import('@/views/Settings.vue'),
})

// 移除路由(通过 name)
router.removeRoute('Admin')

// 检查路由是否存在
if (router.hasRoute('Admin')) {
  console.log('Admin 路由存在')
}

典型用法:根据权限动态注册路由

ts
// router/guards.ts
const asyncRoutes = [
  { path: '/admin', name: 'Admin', roles: ['admin'] },
  { path: '/editor', name: 'Editor', roles: ['admin', 'editor'] },
]

router.beforeEach((to) => {
  const userStore = useUserStore()
  if (!userStore.isLoggedIn) return { name: 'Login' }

  // 根据角色动态添加路由
  if (!userStore.routesLoaded) {
    asyncRoutes
      .filter((route) => route.roles.includes(userStore.role))
      .forEach((route) => router.addRoute(route))
    userStore.routesLoaded = true
    return { path: to.fullPath, replace: true } // 重新导航以匹配新路由
  }
})

router.resolve()

获取路由解析结果,不触发导航。

ts
const resolved = router.resolve({ name: 'User', params: { id: 123 } })
console.log(resolved.href)   // "/user/123"
console.log(resolved.fullPath) // "/user/123"
console.log(resolved.matched) // 匹配的路由记录

常见坑点

1. 重复导航报错

ts
// ❌ NavigationDuplicated
router.push({ name: 'Home' })

// ✅ 捕获错误
router.push({ name: 'Home' }).catch((err) => {
  if (isNavigationFailure(err)) return
  throw err
})

2. 组合式 API 中获取路由

vue
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'

// ✅ 在 setup 中使用
const router = useRouter()
const route = useRoute()

// ❌ 不要在模板中直接使用 $route(Options API 除外)
</script>

3. params 与 query 的区别

ts
// params —— 路径的一部分
router.push({ name: 'User', params: { id: 123 } })
// URL: /user/123

// query —— 查询参数
router.push({ path: '/search', query: { keyword: 'vue' } })
// URL: /search?keyword=vue

// ⚠️ params 不能和 path 一起使用,必须用 name

4. 路由组件复用问题

vue
<!-- 同一组件,不同参数,组件会复用 -->
<!-- /user/1 → /user/2,组件不销毁重建 -->

<script setup lang="ts">
import { watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

// 方案一:watch 监听参数变化
watch(
  () => route.params.id,
  (newId) => {
    fetchUser(newId as string)
  }
)

// 方案二:onBeforeRouteUpdate
import { onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteUpdate((to) => {
  fetchUser(to.params.id as string)
})

// 方案三:给 router-view 添加 key(强制重建,性能差)
// <router-view :key="$route.fullPath" />
</script>

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准