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 router2. 动态路由
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. 嵌套路由不显示
问题:嵌套路由组件不显示
解决方案:
- 确保父组件中包含
<router-view> - 检查子路由路径是否正确
- 检查路由配置是否正确
3. 路由守卫不执行
问题:路由守卫没有执行
解决方案:
- 检查守卫类型是否正确
- 确保调用了
next() - 检查路由配置是否正确
4. 动态路由参数不更新
问题:动态路由参数变化时,组件没有更新
解决方案:
- 使用
watch监听路由参数变化 - 使用
onBeforeRouteUpdate守卫 - 确保组件正确处理参数变化
5. 懒加载失败
问题:懒加载路由组件失败
解决方案:
- 检查组件路径是否正确
- 检查 Webpack 配置是否正确
- 检查是否使用了正确的注释语法
6. 滚动行为不生效
问题:滚动行为配置正确,但不生效
解决方案:
- 检查是否使用了正确的历史模式
- 检查滚动行为函数是否返回了正确的对象
- 检查浏览器是否支持平滑滚动
进阶学习资源
1. 官方文档
2. 工具和库
3. 最佳实践指南
4. 学习案例
实践练习
练习 1:基本路由配置
- 创建一个 Vue 3 项目
- 安装和配置 Vue Router
- 创建基本路由
- 测试路由导航
练习 2:动态路由和嵌套路由
- 实现动态路由
- 实现嵌套路由
- 测试动态路由参数
- 测试嵌套路由
练习 3:路由守卫
- 实现全局守卫
- 实现路由独享守卫
- 实现组件内守卫
- 测试路由守卫
练习 4:路由元信息和认证
- 配置路由元信息
- 实现认证逻辑
- 使用路由守卫处理认证
- 测试认证流程
练习 5:路由懒加载和滚动行为
- 实现路由懒加载
- 配置滚动行为
- 测试懒加载性能
- 测试滚动行为
练习 6:404 页面和错误处理
- 实现 404 页面
- 实现捕获所有路由
- 测试错误处理
总结
Vue Router 是 Vue 3 官方的路由管理器,用于构建单页应用的导航系统。通过掌握 Vue Router 的高级路由模式,包括动态路由、嵌套路由、路由守卫、导航守卫、路由懒加载等,你可以构建可扩展、可维护的路由系统。在实际开发中,我们需要根据项目需求选择合适的路由模式,并遵循最佳实践,确保路由系统的性能和可维护性。
下一集我们将学习 Vue 3 与 Nuxt 3 全栈框架,敬请期待!