跳到主要内容

Vue Router 路由

Vue Router 是 Vue.js 官方的路由管理器,用于构建单页面应用(SPA)。本章将详细介绍 Vue Router 4 的使用方法,从基础配置到高级功能。

基础概念

什么是路由?

路由是指根据 URL 路径显示不同内容的技术。在单页面应用中,路由允许在不刷新页面的情况下切换视图:

用户访问 /about


┌──────────────┐
│ Router │ 匹配路由配置
└──────────────┘


┌──────────────┐
│ Component │ 渲染对应组件
└──────────────┘

安装 Vue Router

使用 Vite 创建项目时可以选择安装,或手动安装:

npm install vue-router

基本使用

创建路由配置

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

// 定义路由配置
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
// 动态路由参数
path: '/user/:id',
name: 'User',
component: () => import('../views/User.vue') // 懒加载
}
]

// 创建路由实例
const router = createRouter({
// 使用 HTML5 History 模式
history: createWebHistory(),
routes
})

export default router

路由模式

Vue Router 提供两种路由模式:

import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'

// HTML5 History 模式(推荐)
// URL 形式: https://example.com/user/123
const router = createRouter({
history: createWebHistory(),
routes
})

// Hash 模式
// URL 形式: https://example.com/#/user/123
const router = createRouter({
history: createWebHashHistory(),
routes
})

两种模式的区别

特性History 模式Hash 模式
URL 美观✅ 更美观有 # 号
服务端配置需要配置不需要
SEO 友好✅ 更友好较差
兼容性需要现代浏览器✅ 更好
提示

使用 History 模式时,需要服务端配置,确保所有路径都返回 index.html,否则刷新页面会出现 404。

在应用中使用

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

const app = createApp(App)
app.use(router)
app.mount('#app')
<!-- App.vue -->
<script setup>
import { RouterView, RouterLink } from 'vue-router'
</script>

<template>
<nav>
<RouterLink to="/">首页</RouterLink>
<RouterLink to="/about">关于</RouterLink>
</nav>

<!-- 路由匹配的组件将渲染在这里 -->
<RouterView />
</template>

路由链接

RouterLink 是 Vue Router 提供的导航组件,它会自动处理路由跳转:

<template>
<!-- 基本用法 -->
<RouterLink to="/">首页</RouterLink>

<!-- 动态路径 -->
<RouterLink :to="{ name: 'User', params: { id: 1 }}">
用户1
</RouterLink>

<!-- 带查询参数 -->
<RouterLink :to="{ path: '/search', query: { q: 'vue' }}">
搜索
</RouterLink>

<!-- 替换当前历史记录 -->
<RouterLink to="/about" replace>关于</RouterLink>

<!-- 自定义激活类名 -->
<RouterLink
to="/"
active-class="active"
exact-active-class="exact-active"
>
首页
</RouterLink>

<!-- 自定义标签 -->
<RouterLink to="/" custom v-slot="{ navigate, isActive }">
<button @click="navigate" :class="{ active: isActive }">
首页
</button>
</RouterLink>
</template>

编程式导航

在 JavaScript 代码中进行路由跳转:

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

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

// 跳转到指定路径
router.push('/about')

// 跳转到命名路由
router.push({ name: 'User', params: { id: 1 }})

// 跳转到带查询参数
router.push({ path: '/search', query: { q: 'vue' }})

// 替换当前记录(不留历史记录)
router.replace('/about')

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

// 获取当前路由信息
console.log(route.path) // 当前路径
console.log(route.params) // 路由参数
console.log(route.query) // 查询参数
console.log(route.hash) // 哈希值
console.log(route.name) // 路由名称
console.log(route.meta) // 路由元信息

动态路由

路由参数

动态路由使用冒号 : 标记参数:

const routes = [
// 单个参数
{ path: '/user/:id', component: User },

// 多个参数
{ path: '/article/:category/:id', component: Article },

// 可选参数(Vue Router 4+)
{ path: '/user/:id?', component: User },

// 可重复参数
{ path: '/files/:path*', component: Files }, // 匹配 /files/a/b/c
{ path: '/files/:path+', component: Files }, // 至少匹配一段
]

参数匹配规则

模式匹配路径route.params
/user/:id/user/123{ id: '123' }
/user/:id?/user/user/123{}{ id: '123' }
/files/:path*/files/files/a/b{}{ path: ['a', 'b'] }
/files/:path+/files/a/files/a/b{ path: ['a'] }{ path: ['a', 'b'] }

在组件中获取参数

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

const route = useRoute()

// 方式1:直接访问
const userId = computed(() => route.params.id)

// 方式2:监听参数变化
watch(
() => route.params.id,
(newId, oldId) => {
console.log(`用户ID从 ${oldId} 变为 ${newId}`)
// 重新获取用户数据
fetchUser(newId)
}
)

// 获取查询参数
const searchQuery = computed(() => route.query.q)

// 获取哈希值
const hash = computed(() => route.hash)
</script>

<template>
<div>
<p>用户ID: {{ userId }}</p>
<p>搜索词: {{ searchQuery }}</p>
</div>
</template>

响应参数变化

当从 /user/1 导航到 /user/2 时,相同的组件实例会被复用,生命周期钩子不会重新触发。需要监听参数变化来响应:

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

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

// 方式1:使用 watch
watch(
() => route.params.id,
async (newId) => {
user.value = await fetchUser(newId)
},
{ immediate: true }
)

// 方式2:使用导航守卫
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
user.value = await fetchUser(to.params.id)
}
})
</script>

404 页面

使用通配符捕获所有未匹配的路由:

const routes = [
// 其他路由...

// 匹配所有路径(放在最后)
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}
]
<!-- NotFound.vue -->
<script setup>
import { useRoute, useRouter } from 'vue-router'

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

// 获取未匹配的路径
const unmatchedPath = route.params.pathMatch
</script>

<template>
<div class="not-found">
<h1>404</h1>
<p>页面不存在</p>
<p>路径: /{{ unmatchedPath?.join('/') }}</p>
<button @click="router.push('/')">返回首页</button>
</div>
</template>

嵌套路由

当应用有多层嵌套的组件结构时,可以使用嵌套路由:

const routes = [
{
path: '/user/:id',
component: UserLayout,
children: [
{
// 默认子路由(匹配 /user/:id)
path: '',
name: 'UserProfile',
component: UserProfile
},
{
// 匹配 /user/:id/posts
path: 'posts',
name: 'UserPosts',
component: UserPosts
},
{
// 匹配 /user/:id/settings
path: 'settings',
name: 'UserSettings',
component: UserSettings
}
]
}
]
<!-- UserLayout.vue -->
<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()
const userId = computed(() => route.params.id)
</script>

<template>
<div class="user-layout">
<nav>
<RouterLink :to="{ name: 'UserProfile' }">个人信息</RouterLink>
<RouterLink :to="{ name: 'UserPosts' }">帖子</RouterLink>
<RouterLink :to="{ name: 'UserSettings' }">设置</RouterLink>
</nav>

<!-- 子路由组件将渲染在这里 -->
<RouterView />
</div>
</template>

命名视图

当需要同时显示多个视图(而不是嵌套)时,可以使用命名视图:

基本用法

<!-- App.vue -->
<template>
<div class="layout">
<!-- 侧边栏视图 -->
<RouterView name="sidebar" class="sidebar" />

<!-- 默认视图(没有 name 属性) -->
<RouterView class="main" />

<!-- 右侧栏视图 -->
<RouterView name="right" class="right" />
</div>
</template>
const routes = [
{
path: '/',
components: {
default: Home, // 默认视图
sidebar: Sidebar, // name="sidebar" 的视图
right: RightSidebar // name="right" 的视图
}
}
]

嵌套命名视图

可以组合使用嵌套路由和命名视图创建复杂布局:

<!-- SettingsLayout.vue -->
<template>
<div class="settings-layout">
<nav>
<RouterLink to="/settings/emails">邮件订阅</RouterLink>
<RouterLink to="/settings/profile">个人资料</RouterLink>
</nav>

<!-- 主内容区 -->
<RouterView />

<!-- 辅助视图 -->
<RouterView name="helper" />
</div>
</template>
const routes = [
{
path: '/settings',
component: SettingsLayout,
children: [
{
path: 'emails',
component: EmailSettings
},
{
path: 'profile',
components: {
default: ProfileSettings,
helper: ProfilePreview
}
}
]
}
]

路由守卫

路由守卫用于在导航过程中进行权限控制、数据加载等操作。

全局前置守卫

使用 router.beforeEach 注册全局前置守卫:

const router = createRouter({ ... })

router.beforeEach((to, from) => {
// to: 目标路由对象
// from: 来源路由对象

// 返回值:
// - false: 取消导航
// - 路由地址: 重定向到该地址
// - true 或 undefined: 继续导航

// 检查是否需要登录
if (to.meta.requiresAuth && !isAuthenticated()) {
// 重定向到登录页
return { name: 'Login', query: { redirect: to.fullPath } }
}

// 允许导航
return true
})

// 支持异步
router.beforeEach(async (to, from) => {
// 可以返回 Promise
const canAccess = await checkAccess(to)
if (!canAccess) {
return false
}
})

全局解析守卫

router.beforeResolve 在导航确认之前、所有组件内守卫和异步组件解析之后调用:

router.beforeResolve(async (to) => {
// 确保用户有摄像头权限
if (to.meta.requiresCamera) {
try {
await navigator.mediaDevices.getUserMedia({ video: true })
} catch (error) {
return false
}
}
})

全局后置钩子

router.afterEach 在导航完成后调用,无法影响导航:

router.afterEach((to, from, failure) => {
// failure: 导航失败信息(如果导航成功则为 null)

// 设置页面标题
document.title = to.meta.title || '默认标题'

// 发送页面访问统计
analytics.trackPageView(to.fullPath)
})

路由独享守卫

直接在路由配置中定义守卫:

const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from) => {
// 只有在进入 /admin 时才会触发
// 不会在子路由之间切换时触发
if (!isAdmin()) {
return { name: 'Home' }
}
}
}
]

// 可以使用函数数组
function removeQueryParams(to) {
if (Object.keys(to.query).length) {
return { path: to.path, query: {} }
}
}

function removeHash(to) {
if (to.hash) {
return { path: to.path, hash: '' }
}
}

const routes = [
{
path: '/about',
component: About,
beforeEnter: [removeQueryParams, removeHash]
}
]

组件内守卫

在组件内部使用导航守卫:

<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// 路由参数变化时触发
onBeforeRouteUpdate(async (to, from) => {
// 可以访问组件实例
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})

// 离开路由时触发
onBeforeRouteLeave((to, from) => {
// 提示用户保存未保存的更改
if (hasUnsavedChanges.value) {
const answer = window.confirm('有未保存的更改,确定要离开吗?')
if (!answer) return false
}
})
</script>

守卫执行顺序

完整的导航解析流程:

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

路由元信息

使用 meta 字段为路由添加额外信息:

const routes = [
{
path: '/admin',
component: Admin,
meta: {
requiresAuth: true,
roles: ['admin'],
title: '管理后台'
}
},
{
path: '/profile',
component: Profile,
meta: {
requiresAuth: true,
title: '个人资料'
}
}
]

在守卫中使用元信息

router.beforeEach((to, from) => {
// 设置页面标题
document.title = to.meta.title || '默认标题'

// 检查权限
if (to.meta.requiresAuth && !isAuthenticated()) {
return { name: 'Login' }
}

// 检查角色
if (to.meta.roles && !to.meta.roles.includes(currentUser.role)) {
return { name: 'Forbidden' }
}
})

类型安全的元信息(TypeScript)

// 定义元信息类型
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: string[]
title?: string
}
}

滚动行为

使用 scrollBehavior 函数控制路由切换时的滚动位置:

const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 返回滚动位置
}
})

基本用法

const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { top: 0 }
}
})

返回历史记录位置

const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 如果是通过浏览器的后退/前进按钮触发,返回保存的位置
if (savedPosition) {
return savedPosition
}
// 否则滚动到顶部
return { top: 0 }
}
})

滚动到锚点

const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 如果有 hash,滚动到锚点
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth' // 平滑滚动
}
}
}
})

滚动到特定元素

const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 滚动到 id="main" 的元素,并偏移 10px
return {
el: '#main',
top: 10,
behavior: 'smooth'
}
}
})

延迟滚动

有时需要等待页面渲染完成后再滚动:

const router = createRouter({
scrollBehavior(to, from, savedPosition) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ top: 0 })
}, 500)
})
}
})

路由懒加载

使用动态导入实现路由懒加载,优化应用性能:

// 直接导入(不推荐)
import Home from './views/Home.vue'

// 懒加载(推荐)
const Home = () => import('./views/Home.vue')

// 带命名 chunk 的懒加载
const Home = () => import(/* webpackChunkName: "home" */ './views/Home.vue')

// 将多个路由打包到同一个 chunk
const UserPosts = () => import(/* webpackChunkName: "user" */ './views/UserPosts.vue')
const UserSettings = () => import(/* webpackChunkName: "user" */ './views/UserSettings.vue')

分组打包示例

const routes = [
{
path: '/admin',
component: () => import(/* webpackChunkName: "admin" */ './views/Admin.vue'),
children: [
{
path: 'users',
component: () => import(/* webpackChunkName: "admin" */ './views/AdminUsers.vue')
},
{
path: 'settings',
component: () => import(/* webpackChunkName: "admin" */ './views/AdminSettings.vue')
}
]
}
]

动态路由

添加路由

使用 router.addRoute() 动态添加路由:

// 添加单个路由
router.addRoute({
path: '/new-route',
component: NewRoute
})

// 添加嵌套路由
router.addRoute('parentRoute', {
path: 'child',
component: ChildRoute
})

删除路由

// 通过名称删除路由
router.removeRoute('routeName')

// 添加时返回删除函数
const removeRoute = router.addRoute(routeConfig)
removeRoute() // 删除该路由

查看现有路由

// 获取所有路由
router.getRoutes()
// 返回 RouteRecordNormalized 数组

// 检查路由是否存在
router.hasRoute('routeName')

实际应用:权限路由

// 根据用户权限动态添加路由
async function setupRoutes(user) {
const routes = await fetchUserRoutes(user.id)

routes.forEach(route => {
router.addRoute(route)
})

// 添加一个通配符路由,确保权限检查后才匹配
router.addRoute({
path: '/:pathMatch(.*)*',
redirect: '/404'
})
}

路由组件传参

使用 props 解耦

const routes = [
// 布尔模式:将 route.params 设置为组件 props
{
path: '/user/:id',
component: User,
props: true
},

// 对象模式:直接传递静态值
{
path: '/about',
component: About,
props: { title: '关于我们' }
},

// 函数模式:动态计算 props
{
path: '/search',
component: Search,
props: (route) => ({
query: route.query.q,
page: Number(route.query.page) || 1
})
}
]
<!-- User.vue -->
<script setup>
// 直接作为 props 接收,无需使用 route.params
defineProps({
id: String
})
</script>

<template>
<div>用户 ID: {{ id }}</div>
</template>

过渡动画

为路由切换添加过渡效果:

<template>
<RouterView v-slot="{ Component, route }">
<Transition name="fade" mode="out-in">
<component :is="Component" :key="route.path" />
</Transition>
</RouterView>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

基于路由的动态过渡

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

const route = useRoute()
const transitionName = ref('fade')

watch(
() => route.meta.transition,
(newTransition) => {
transitionName.value = newTransition || 'fade'
}
)
</script>

<template>
<RouterView v-slot="{ Component }">
<Transition :name="transitionName" mode="out-in">
<component :is="Component" />
</Transition>
</RouterView>
</template>

小结

本章我们详细学习了 Vue Router 的完整内容:

  1. 路由基础:创建路由配置和使用路由
  2. 路由模式:History 模式和 Hash 模式的区别
  3. 路由链接:RouterLink 和编程式导航
  4. 动态路由:路由参数、可选参数、可重复参数
  5. 嵌套路由:多级路由结构
  6. 命名视图:同时显示多个视图
  7. 路由守卫:全局守卫、路由独享守卫、组件内守卫
  8. 路由元信息:为路由添加额外信息
  9. 滚动行为:控制路由切换时的滚动位置
  10. 路由懒加载:优化应用性能
  11. 动态路由:运行时添加/删除路由

练习

  1. 创建一个带侧边栏的后台管理页面,使用命名视图
  2. 实现登录验证和权限控制,使用路由守卫
  3. 实现路由懒加载,并添加加载状态指示器
  4. 创建一个动态路由系统,根据用户权限显示不同菜单

参考资源