在 Vue 3 项目中,路由管理是构建单页应用的核心环节。组合式 API 提供的 useRouter 和 useRoute 函数,彻底改变了传统选项式 API 的路由使用方式。这两个函数不仅简化了代码结构,更通过响应式系统实现了路由信息的实时同步。
Vue Router 4 专为 Vue 3 设计,其核心原理建立在 Vue 的响应式系统之上。当我们在组件中调用 useRoute() 时,返回的是一个响应式的路由对象(RouteLocationNormalized)。这意味着任何对路由属性的访问(如 route.params、route.query)都会自动建立依赖关系,当路由变化时,相关组件会自动更新。
useRouter() 则返回路由器的实例,提供编程式导航方法。与响应式路由对象不同,路由器实例本身不是响应式的,但其方法会触发路由变化,进而驱动响应式系统更新。
提示:在
<script setup>语法糖中,无需额外处理即可直接使用这两个函数,因为 setup 上下文已自动注入路由依赖。
现代 Vue 项目通常使用 Vite 作为构建工具。以下是完整的路由配置示例:
javascript复制// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView,
meta: {
title: '首页'
}
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
meta: {
requiresAuth: true
}
},
{
path: '/user/:userId',
name: 'user',
component: () => import('../views/UserView.vue'),
props: true // 启用 props 传参
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
return savedPosition || { top: 0 }
}
})
// 全局前置守卫
router.beforeEach((to) => {
document.title = to.meta.title || '默认标题'
})
export default router
关键配置说明:
createWebHistory 使用 HTML5 History 模式,需服务器端支持() => import())实现路由级代码分割props: true 将路由参数自动转换为组件 propsscrollBehavior 控制页面滚动位置useRouter() 返回的 router 实例提供了多种导航控制方法:
javascript复制const router = useRouter()
// 基本跳转
router.push('/about')
// 命名路由跳转
router.push({ name: 'user', params: { userId: 123 } })
// 带查询参数
router.push({ path: '/search', query: { q: 'vue' } })
// 替换当前路由(不添加历史记录)
router.replace({ path: '/login' })
// 前进/后退
router.go(1)
router.go(-1)
实际开发中,我们通常会封装导航操作:
javascript复制// utils/navigation.js
export const useNavigation = () => {
const router = useRouter()
const navigateToUser = (userId) => {
router.push({
name: 'user',
params: { userId },
query: { from: 'dashboard' }
})
}
return { navigateToUser }
}
useRoute() 返回的 route 对象包含以下常用属性:
javascript复制const route = useRoute()
// 路径信息
route.path // 当前路径 (e.g. "/user/123")
route.fullPath // 完整路径 (e.g. "/user/123?from=dashboard")
route.name // 路由名称
// 参数
route.params // 动态参数 (e.g. { userId: "123" })
route.query // 查询参数 (e.g. { from: "dashboard" })
// 元信息
route.meta // 路由元数据
route.hash // URL hash (e.g. "#section-1")
在模板中使用时,Vue 会自动建立响应式依赖:
html复制<template>
<div>
<h1>用户ID: {{ route.params.userId }}</h1>
<p>来自: {{ route.query.from || '未知来源' }}</p>
</div>
</template>
大型应用通常需要动态路由和权限控制。以下是一个完整的权限路由方案:
javascript复制// router/index.js
const dynamicRoutes = [
{
path: '/admin',
name: 'admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAdmin: true }
}
]
export function setupRouter(app) {
const router = createRouter({ /* 基础配置 */ })
// 重置路由函数
function resetRouter() {
router.getRoutes().forEach((route) => {
if (route.name) {
router.removeRoute(route.name)
}
})
}
// 动态添加路由
function addDynamicRoutes(userRole) {
if (userRole === 'admin') {
dynamicRoutes.forEach(route => {
router.addRoute(route)
})
}
}
// 全局前置守卫
router.beforeEach(async (to) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } }
}
if (to.meta.requiresAdmin && !userStore.isAdmin) {
return { name: 'forbidden' }
}
})
app.use(router)
return { router, resetRouter, addDynamicRoutes }
}
Vite 和 Webpack 都支持路由级代码分割:
javascript复制const routes = [
{
path: '/dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue'),
meta: { preload: true } // 标记需要预加载的路由
}
]
可以结合路由元信息和 Intersection Observer 实现智能预加载:
javascript复制// main.js
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const route = router.resolve(entry.target.dataset.routePath)
if (route.meta.preload) {
import(/* @vite-ignore */ route.component.file)
}
}
})
})
// 在组件中使用
<div data-route-path="/dashboard"></div>
通过扩展 RouteMeta 接口增强类型检查:
typescript复制// types/router.d.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
requiresAdmin?: boolean
title?: string
transition?: string
}
}
为动态路由参数定义严格类型:
typescript复制// router/index.ts
const routes: RouteRecordRaw[] = [
{
path: '/user/:userId',
name: 'user',
component: () => import('@/views/UserView.vue'),
props: (route) => ({
userId: Number(route.params.userId),
from: String(route.query.from) || undefined
})
}
]
// views/UserView.vue
<script setup lang="ts">
defineProps<{
userId: number
from?: string
}>()
</script>
解决方案:使用 watch 显式监听参数变化
javascript复制import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watch(
() => route.params.userId,
(newId) => {
fetchUserData(newId)
},
{ immediate: true }
)
解决方案:自定义 router 方法处理错误
javascript复制// utils/router.js
export function useSafeRouter() {
const router = useRouter()
const safePush = (location) => {
return router.push(location).catch(err => {
if (err.name !== 'NavigationDuplicated') {
throw err
}
})
}
return { ...router, push: safePush }
}
完整解决方案:
javascript复制const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth'
}
}
// 处理特定路由的滚动位置
if (to.meta.scrollToTop !== false) {
return { top: 0 }
}
}
})
typescript复制// stores/router.ts
import { defineStore } from 'pinia'
export const useRouterStore = defineStore('router', {
state: () => ({
previousRoute: null as string | null,
currentRoute: '/'
}),
actions: {
setRoute(from: string, to: string) {
this.previousRoute = from
this.currentRoute = to
}
}
})
// 在导航守卫中使用
router.afterEach((to, from) => {
const routerStore = useRouterStore()
routerStore.setRoute(from.path, to.path)
})
typescript复制// stores/user.ts
export const useUserStore = defineStore('user', {
actions: {
async fetchUser(userId: string) {
// 获取用户数据
}
}
})
// 在组件中
watch(
() => route.params.userId,
(userId) => {
if (userId) {
useUserStore().fetchUser(userId)
}
},
{ immediate: true }
)
使用 vue-router-mock 进行组件测试:
javascript复制import { mount } from '@vue/test-utils'
import { createRouterMock } from 'vue-router-mock'
const router = createRouterMock({
routes: [
{ path: '/', name: 'home' },
{ path: '/user/:id', name: 'user' }
]
})
test('navigates to user page', async () => {
const wrapper = mount(UserComponent, {
global: {
plugins: [router]
}
})
await wrapper.find('button').trigger('click')
expect(router.push).toHaveBeenCalledWith({ name: 'user', params: { id: '123' } })
})
在开发工具中检查路由状态:
javascript复制// 在控制台访问当前路由
const route = useRoute()
console.log('当前路由:', route)
// 检查所有已注册路由
console.log('路由表:', router.getRoutes())
// 模拟导航
router.push('/debug-route')
javascript复制{
path: '/product/:productId(\\d+)',
name: 'product',
component: () => import('@/views/ProductDetail.vue'),
props: route => ({
productId: Number(route.params.productId),
variantId: route.query.variant ? Number(route.query.variant) : null
}),
meta: {
breadcrumb: '商品详情',
scrollToTop: true
}
}
javascript复制router.beforeEach(async (to) => {
if (to.meta.requiresCart) {
const cartStore = useCartStore()
await cartStore.loadCart()
if (cartStore.isEmpty) {
return { name: 'cart-empty' }
}
}
})
// 路由配置
{
path: '/checkout',
name: 'checkout',
component: () => import('@/views/Checkout.vue'),
meta: { requiresCart: true }
}
javascript复制router.beforeEach((to, from, next) => {
const startTime = performance.now()
next()
const duration = performance.now() - startTime
if (duration > 500) {
trackSlowNavigation(to.fullPath, duration)
}
})
javascript复制router.onError((error) => {
if (error.message.includes('Failed to fetch dynamically imported module')) {
showErrorToast('加载页面失败,请刷新重试')
router.push('/error?code=load_failed')
}
})
html复制<template>
<router-view v-slot="{ Component }">
<transition :name="route.meta.transition || 'fade'">
<component :is="Component" />
</transition>
</router-view>
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
javascript复制// components/ScrollPersist.vue
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const scrollPositions = new Map()
onMounted(() => {
window.addEventListener('scroll', savePosition)
restorePosition()
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', savePosition)
})
function savePosition() {
scrollPositions.set(route.fullPath, window.scrollY)
}
function restorePosition() {
const y = scrollPositions.get(route.fullPath) || 0
window.scrollTo(0, y)
}
</script>
在开发 Vue 3 项目时,合理使用 useRouter 和 useRoute 可以大幅提升代码的可维护性和开发效率。特别是在大型项目中,结合 TypeScript 和状态管理,能够构建出健壮且易于扩展的路由系统。实际开发中,建议根据项目规模选择合适的路由组织方式,小型项目可以使用扁平化路由结构,而大型项目则适合采用模块化路由配置。