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 组件
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>
守卫执行顺序
完整的导航解析流程:
- 导航被触发
- 在失活的组件里调用
onBeforeRouteLeave - 调用全局的
beforeEach - 在重用的组件里调用
onBeforeRouteUpdate - 在路由配置里调用
beforeEnter - 解析异步路由组件
- 在被激活的组件里调用
beforeRouteEnter - 调用全局的
beforeResolve - 导航被确认
- 调用全局的
afterEach - 触发 DOM 更新
- 调用
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 的完整内容:
- 路由基础:创建路由配置和使用路由
- 路由模式:History 模式和 Hash 模式的区别
- 路由链接:RouterLink 和编程式导航
- 动态路由:路由参数、可选参数、可重复参数
- 嵌套路由:多级路由结构
- 命名视图:同时显示多个视图
- 路由守卫:全局守卫、路由独享守卫、组件内守卫
- 路由元信息:为路由添加额外信息
- 滚动行为:控制路由切换时的滚动位置
- 路由懒加载:优化应用性能
- 动态路由:运行时添加/删除路由
练习
- 创建一个带侧边栏的后台管理页面,使用命名视图
- 实现登录验证和权限控制,使用路由守卫
- 实现路由懒加载,并添加加载状态指示器
- 创建一个动态路由系统,根据用户权限显示不同菜单