Vue 3 与 Vue Router 高级路由模式

概述

Vue Router 是 Vue 3 官方的路由管理器,用于构建单页应用(SPA)的导航系统。它允许你定义路由规则,将 URL 映射到组件,并提供导航控制、路由守卫、嵌套路由等功能。在实际开发中,我们经常需要处理复杂的路由场景,如动态路由、嵌套路由、路由守卫、导航守卫、路由懒加载等。本集将深入探讨 Vue Router 的高级路由模式,帮助你构建可扩展、可维护的路由系统。

核心知识点

1. Vue Router 基本概念

1.1 核心组件

Vue Router 包含以下核心组件:

  • **<router-link>**:用于导航的链接组件
  • **<router-view>**:用于渲染匹配路由组件的视图组件
  • **router**:路由实例,包含路由配置和导航方法
  • **route**:当前路由对象,包含路由信息

1.2 基本路由配置

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'About',
    component: AboutView
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router

2. 动态路由

2.1 基本动态路由

// src/router/index.js
const routes = [
  // 动态路由
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('../views/UserView.vue')
  },
  // 可选参数
  {
    path: '/user/:id?',
    name: 'User',
    component: () => import('../views/UserView.vue')
  },
  // 多个参数
  {
    path: '/user/:id/post/:postId',
    name: 'UserPost',
    component: () => import('../views/UserPostView.vue')
  }
]

2.2 使用动态路由

<!-- src/views/UserView.vue -->
<template>
  <div class="user">
    <h1>User {{ $route.params.id }}</h1>
    <p>{{ user?.name }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const user = ref(null)

// 初始加载数据
onMounted(() => {
  fetchUser()
})

// 监听路由参数变化
watch(() => route.params.id, () => {
  fetchUser()
})

async function fetchUser() {
  const response = await fetch(`/api/user/${route.params.id}`)
  user.value = await response.json()
}
</script>

3. 嵌套路由

3.1 配置嵌套路由

// src/router/index.js
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('../views/DashboardView.vue'),
    children: [
      {
        path: '', // 嵌套默认路由
        name: 'DashboardHome',
        component: () => import('../views/DashboardHomeView.vue')
      },
      {
        path: 'profile',
        name: 'DashboardProfile',
        component: () => import('../views/DashboardProfileView.vue')
      },
      {
        path: 'settings',
        name: 'DashboardSettings',
        component: () => import('../views/DashboardSettingsView.vue')
      }
    ]
  }
]

3.2 使用嵌套路由

<!-- src/views/DashboardView.vue -->
<template>
  <div class="dashboard">
    <h1>Dashboard</h1>
    <nav>
      <router-link to="/dashboard">Home</router-link>
      <router-link to="/dashboard/profile">Profile</router-link>
      <router-link to="/dashboard/settings">Settings</router-link>
    </nav>
    <!-- 渲染子路由组件 -->
    <router-view />
  </div>
</template>

4. 路由守卫

4.1 全局守卫

// src/router/index.js

// 全局前置守卫
router.beforeEach((to, from, next) => {
  console.log('Before each navigation:', to, from)
  next() // 必须调用 next() 才能继续导航
})

// 全局解析守卫
router.beforeResolve((to, from, next) => {
  console.log('Before resolve navigation:', to, from)
  next()
})

// 全局后置守卫
router.afterEach((to, from) => {
  console.log('After each navigation:', to, from)
})

4.2 路由独享守卫

// src/router/index.js
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('../views/DashboardView.vue'),
    // 路由独享守卫
    beforeEnter: (to, from, next) => {
      console.log('Before entering dashboard:', to, from)
      // 检查用户是否认证
      if (!isAuthenticated()) {
        next({ name: 'Login' })
      } else {
        next()
      }
    }
  }
]

function isAuthenticated() {
  // 检查用户是否认证的逻辑
  return localStorage.getItem('token') !== null
}

4.3 组件内守卫

<!-- src/views/DashboardView.vue -->
<script setup>
import { ref } from 'vue'

// 组件内守卫
const beforeRouteEnter = (to, from, next) => {
  console.log('Before route enter:', to, from)
  // 无法访问 this,因为组件实例还未创建
  next((vm) => {
    // 可以通过 vm 访问组件实例
    console.log('Component instance:', vm)
  })
}

const beforeRouteUpdate = (to, from, next) => {
  console.log('Before route update:', to, from)
  // 可以访问 this,因为组件实例已经存在
  next()
}

const beforeRouteLeave = (to, from, next) => {
  console.log('Before route leave:', to, from)
  // 可以访问 this,因为组件实例已经存在
  next()
}
</script>

5. 路由元信息

5.1 配置路由元信息

// src/router/index.js
const routes = [
  {
    path: '/',
    name: 'Home',
    component: HomeView,
    meta: {
      title: 'Home Page',
      requiresAuth: false,
      layout: 'default'
    }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('../views/DashboardView.vue'),
    meta: {
      title: 'Dashboard',
      requiresAuth: true,
      layout: 'dashboard'
    }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('../views/AdminView.vue'),
    meta: {
      title: 'Admin Panel',
      requiresAuth: true,
      requiresAdmin: true,
      layout: 'admin'
    }
  }
]

5.2 使用路由元信息

// src/router/index.js

// 全局前置守卫,处理认证和标题
router.beforeEach((to, from, next) => {
  // 设置页面标题
  document.title = to.meta.title || 'Default Title'
  
  // 检查是否需要认证
  if (to.meta.requiresAuth) {
    if (!isAuthenticated()) {
      next({ name: 'Login', query: { redirect: to.fullPath } })
      return
    }
    
    // 检查是否需要管理员权限
    if (to.meta.requiresAdmin && !isAdmin()) {
      next({ name: 'Dashboard' })
      return
    }
  }
  
  next()
})

function isAuthenticated() {
  return localStorage.getItem('token') !== null
}

function isAdmin() {
  return localStorage.getItem('role') === 'admin'
}

6. 路由懒加载

6.1 基本懒加载

// src/router/index.js
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/AboutView.vue')
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('../views/DashboardView.vue')
  }
]

6.2 分组懒加载

// src/router/index.js
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/HomeView.vue')
  },
  // 分组懒加载,将多个路由组件打包到同一个 chunk
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ '../views/DashboardView.vue')
  },
  {
    path: '/dashboard/profile',
    name: 'DashboardProfile',
    component: () => import(/* webpackChunkName: "dashboard" */ '../views/DashboardProfileView.vue')
  },
  {
    path: '/dashboard/settings',
    name: 'DashboardSettings',
    component: () => import(/* webpackChunkName: "dashboard" */ '../views/DashboardSettingsView.vue')
  }
]

7. 导航方法

7.1 编程式导航

<!-- src/components/Navigation.vue -->
<template>
  <div class="navigation">
    <button @click="goToHome">Go to Home</button>
    <button @click="goToAbout">Go to About</button>
    <button @click="goToUser">Go to User</button>
    <button @click="goBack">Go Back</button>
    <button @click="goForward">Go Forward</button>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

const goToHome = () => {
  router.push('/')
}

const goToAbout = () => {
  router.push({ name: 'About' })
}

const goToUser = () => {
  router.push({ name: 'User', params: { id: 123 } })
}

const goBack = () => {
  router.go(-1)
}

const goForward = () => {
  router.go(1)
}
</script>

7.2 导航守卫中的导航

// src/router/index.js
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    // 重定向到登录页,并保留当前路径用于登录后返回
    next({ name: 'Login', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

// src/views/LoginView.vue
<script setup>
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()
const email = ref('')
const password = ref('')

const login = async () => {
  try {
    // 登录逻辑
    // ...
    
    // 获取重定向路径,默认为首页
    const redirect = route.query.redirect || '/'
    router.push(redirect)
  } catch (error) {
    console.error('Login failed:', error)
  }
}
</script>

8. 路由滚动行为

8.1 基本滚动配置

// src/router/index.js
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 返回 savedPosition,保持滚动位置
    if (savedPosition) {
      return savedPosition
    }
    // 滚动到顶部
    return { top: 0 }
  }
})

8.2 高级滚动配置

// src/router/index.js
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 返回 savedPosition,保持滚动位置
    if (savedPosition) {
      return savedPosition
    }
    
    // 滚动到指定元素
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth' // 平滑滚动
      }
    }
    
    // 滚动到顶部
    return { top: 0, behavior: 'smooth' }
  }
})

9. 路由错误处理

9.1 404 页面

// src/router/index.js
const routes = [
  // 其他路由
  // ...
  // 404 页面,必须放在最后
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFoundView.vue')
  }
]

9.2 捕获所有路由

// src/router/index.js
const routes = [
  // 捕获所有路由,包括嵌套路由
  {
    path: '/user-:afterUser(.*)',
    name: 'UserCatchAll',
    component: () => import('../views/UserCatchAllView.vue')
  }
]

10. 路由状态管理

10.1 结合 Pinia 使用

// src/stores/routerStore.js
import { defineStore } from 'pinia'
import { useRoute, useRouter } from 'vue-router'

export const useRouterStore = defineStore('router', () => {
  const route = useRoute()
  const router = useRouter()
  
  // 导航方法
  function navigateTo(path) {
    router.push(path)
  }
  
  function navigateToName(name, params) {
    router.push({ name, params })
  }
  
  function goBack() {
    router.go(-1)
  }
  
  return {
    route,
    navigateTo,
    navigateToName,
    goBack
  }
})

最佳实践

1. 路由模块化

将路由按照功能模块拆分为多个文件,便于维护和扩展:

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import homeRoutes from './modules/home'
import userRoutes from './modules/user'
import adminRoutes from './modules/admin'

const routes = [
  ...homeRoutes,
  ...userRoutes,
  ...adminRoutes,
  // 404 页面
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFoundView.vue')
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router

// src/router/modules/home.js
export default [
  {
    path: '/',
    name: 'Home',
    component: () => import('../../views/HomeView.vue'),
    meta: {
      title: 'Home Page'
    }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../../views/AboutView.vue'),
    meta: {
      title: 'About Page'
    }
  }
]

2. 使用命名路由

使用命名路由可以避免硬编码路径,提高代码的可维护性:

<!-- 推荐 -->
<router-link :to="{ name: 'User', params: { id: 123 } }">User 123</router-link>

<!-- 不推荐 -->
<router-link to="/user/123">User 123</router-link>

3. 懒加载路由组件

懒加载路由组件可以减少初始加载时间,提高应用性能:

// 推荐
{
  path: '/dashboard',
  name: 'Dashboard',
  component: () => import('../views/DashboardView.vue')
}

// 不推荐
import DashboardView from '../views/DashboardView.vue'
{
  path: '/dashboard',
  name: 'Dashboard',
  component: DashboardView
}

4. 使用路由元信息

使用路由元信息可以集中管理路由的配置,如标题、认证要求等:

{
  path: '/dashboard',
  name: 'Dashboard',
  component: () => import('../views/DashboardView.vue'),
  meta: {
    title: 'Dashboard',
    requiresAuth: true,
    layout: 'dashboard'
  }
}

5. 避免嵌套过深

嵌套路由过深会导致组件层级复杂,影响性能和可维护性,建议嵌套层级不超过 3 层:

// 推荐
{
  path: '/dashboard',
  name: 'Dashboard',
  component: DashboardView,
  children: [
    {
      path: '',
      name: 'DashboardHome',
      component: DashboardHomeView
    },
    {
      path: 'profile',
      name: 'DashboardProfile',
      component: DashboardProfileView
    }
  ]
}

// 不推荐(嵌套过深)
{
  path: '/a',
  component: A,
  children: [
    {
      path: 'b',
      component: B,
      children: [
        {
          path: 'c',
          component: C,
          children: [
            {
              path: 'd',
              component: D
            }
          ]
        }
      ]
    }
  ]
}

6. 使用路由守卫处理认证

使用路由守卫集中处理认证逻辑,避免在每个组件中重复编写认证代码:

// src/router/index.js
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth) {
    if (!isAuthenticated()) {
      next({ name: 'Login', query: { redirect: to.fullPath } })
      return
    }
  }
  
  next()
})

常见问题和解决方案

1. 路由不匹配

问题:路由配置正确,但无法匹配

解决方案

  • 检查路由顺序,确保更具体的路由在前面
  • 检查路径是否正确,区分大小写
  • 检查是否使用了正确的历史模式

2. 嵌套路由不显示

问题:嵌套路由组件不显示

解决方案

  • 确保父组件中包含 &lt;router-view&gt;
  • 检查子路由路径是否正确
  • 检查路由配置是否正确

3. 路由守卫不执行

问题:路由守卫没有执行

解决方案

  • 检查守卫类型是否正确
  • 确保调用了 next()
  • 检查路由配置是否正确

4. 动态路由参数不更新

问题:动态路由参数变化时,组件没有更新

解决方案

  • 使用 watch 监听路由参数变化
  • 使用 onBeforeRouteUpdate 守卫
  • 确保组件正确处理参数变化

5. 懒加载失败

问题:懒加载路由组件失败

解决方案

  • 检查组件路径是否正确
  • 检查 Webpack 配置是否正确
  • 检查是否使用了正确的注释语法

6. 滚动行为不生效

问题:滚动行为配置正确,但不生效

解决方案

  • 检查是否使用了正确的历史模式
  • 检查滚动行为函数是否返回了正确的对象
  • 检查浏览器是否支持平滑滚动

进阶学习资源

1. 官方文档

2. 工具和库

3. 最佳实践指南

4. 学习案例

实践练习

练习 1:基本路由配置

  1. 创建一个 Vue 3 项目
  2. 安装和配置 Vue Router
  3. 创建基本路由
  4. 测试路由导航

练习 2:动态路由和嵌套路由

  1. 实现动态路由
  2. 实现嵌套路由
  3. 测试动态路由参数
  4. 测试嵌套路由

练习 3:路由守卫

  1. 实现全局守卫
  2. 实现路由独享守卫
  3. 实现组件内守卫
  4. 测试路由守卫

练习 4:路由元信息和认证

  1. 配置路由元信息
  2. 实现认证逻辑
  3. 使用路由守卫处理认证
  4. 测试认证流程

练习 5:路由懒加载和滚动行为

  1. 实现路由懒加载
  2. 配置滚动行为
  3. 测试懒加载性能
  4. 测试滚动行为

练习 6:404 页面和错误处理

  1. 实现 404 页面
  2. 实现捕获所有路由
  3. 测试错误处理

总结

Vue Router 是 Vue 3 官方的路由管理器,用于构建单页应用的导航系统。通过掌握 Vue Router 的高级路由模式,包括动态路由、嵌套路由、路由守卫、导航守卫、路由懒加载等,你可以构建可扩展、可维护的路由系统。在实际开发中,我们需要根据项目需求选择合适的路由模式,并遵循最佳实践,确保路由系统的性能和可维护性。

下一集我们将学习 Vue 3 与 Nuxt 3 全栈框架,敬请期待!

« 上一篇 Vue 3与Pinia高级状态管理模式 - 现代化状态管理解决方案 下一篇 » Vue 3与Nuxt 3全栈框架 - 现代化Web应用开发解决方案