1. 为什么需要专门了解 useRouter 和 useRoute
在 Vue 3 的组合式 API 中,路由管理是每个开发者必须掌握的核心技能。与 Vue 2 的 Options API 不同,组合式 API 提供了更灵活的方式来组织和复用代码逻辑。useRouter 和 useRoute 这两个函数是 Vue Router 专门为组合式 API 设计的重要工具。
我曾经接手过一个项目,团队还在用 this.$router 和 this.$route 的方式操作路由。当项目规模扩大后,这种写法导致代码难以维护,特别是在需要复用路由逻辑时。组合式 API 的出现完美解决了这个问题。
提示:Vue 3.2 版本后,组合式 API 成为官方推荐写法,性能优化和类型推导都更完善。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. useRouter 和 useRoute 的基本用法
2.1 环境准备与基础配置
首先确保项目已经安装了 Vue Router 4.x(Vue 3 专用版本):
bash复制npm install vue-router@4
在 main.js 中的基础配置:
javascript复制import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
const router = createRouter({
history: createWebHistory(),
routes: [...]
})
const app = createApp(App)
app.use(router)
app.mount('#app')
2.2 在组件中使用
在 setup 函数中引入并使用:
javascript复制import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
// 使用示例
const goHome = () => {
router.push('/')
}
return {
goHome,
currentPath: route.path
}
}
}
注意:useRouter 和 useRoute 必须在 setup() 同步调用,不能在异步回调中直接使用。
3. useRouter 的深度解析
3.1 核心方法与应用场景
useRouter 返回的是 router 实例,常用方法包括:
| 方法 | 说明 | 典型场景 |
|---|---|---|
| push | 导航到新路由 | 点击跳转按钮 |
| replace | 替换当前路由 | 登录后跳转 |
| go | 前进/后退 | 模拟浏览器前进后退 |
| back | 返回上一页 | 返回按钮 |
| forward | 前进一页 | 极少使用 |
实际项目中的典型应用:
javascript复制const handleLogin = async () => {
try {
await loginApi()
// 登录后替换当前路由
router.replace('/dashboard')
} catch (error) {
// 跳转到错误页面并携带参数
router.push({
path: '/error',
query: { code: error.code }
})
}
}
3.2 导航守卫的配合使用
组合式 API 中导航守卫的写法:
javascript复制import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
export default {
setup() {
onBeforeRouteLeave((to, from) => {
return confirm('确定要离开吗?未保存的数据将丢失')
})
onBeforeRouteUpdate(async (to, from) => {
// 路由参数变化时重新获取数据
await fetchData(to.params.id)
})
}
}
4. useRoute 的全面掌握
4.1 路由信息对象详解
useRoute 返回当前路由对象,包含以下关键属性:
javascript复制{
path: '/user/123',
params: { id: '123' }, // 动态路由参数
query: { search: 'vue' }, // URL查询参数
hash: '#profile',
fullPath: '/user/123?search=vue#profile',
matched: [...], // 匹配的路由记录数组
meta: { requiresAuth: true } // 路由元信息
}
4.2 响应式特性的注意事项
route 对象是响应式的,这意味着你可以直接 watch 它的变化:
javascript复制import { watch } from 'vue'
export default {
setup() {
const route = useRoute()
watch(
() => route.params.id,
(newId) => {
fetchUser(newId)
}
)
}
}
但要注意性能问题,避免过度监听。我曾经在一个项目中因为监听了整个 route 对象导致性能下降:
javascript复制// 不推荐写法 - 监听整个route对象
watch(route, (newRoute) => {
// 任何路由变化都会触发
})
// 推荐写法 - 只监听需要的属性
watch(() => route.params.id, handler)
5. 类型安全的最佳实践
5.1 TypeScript 集成
对于使用 TypeScript 的项目,可以定义路由的元数据类型:
typescript复制// router.ts
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: string[]
}
}
const router = createRouter({
routes: [
{
path: '/admin',
meta: { requiresAuth: true, roles: ['admin'] }
}
]
})
在组件中使用时可以获得完整的类型提示:
typescript复制const route = useRoute()
if (route.meta.requiresAuth) {
// 有类型提示
}
5.2 自定义路由类型扩展
对于大型项目,可以进一步扩展路由参数类型:
typescript复制// types/router.d.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteParams {
id: string
projectId: string
}
}
// 组件中使用
const route = useRoute()
const id = route.params.id // 有类型提示为string
6. 常见问题与解决方案
6.1 路由跳转的重复导航错误
当快速连续点击跳转同一个路由时,控制台会出现 NavigationDuplicated 警告。解决方案:
javascript复制// utils/router.js
import { Router } from 'vue-router'
export function setupRouterGuard(router: Router) {
router.isReady().then(() => {
const originalPush = router.push
router.push = function push(location) {
return originalPush.call(this, location).catch(err => {
if (err.name !== 'NavigationDuplicated') throw err
})
}
})
}
6.2 动态路由的缓存问题
使用 keep-alive 时,动态路由组件可能不会正确更新:
vue复制<template>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</template>
<script>
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute()
return { route }
}
}
</script>
6.3 路由初始化时机问题
在 setup 中直接使用路由信息可能会遇到路由未初始化的情况:
javascript复制export default {
async setup() {
const route = useRoute()
// 可能获取不到初始化参数
const initialId = route.params.id
// 推荐写法
await router.isReady()
const initializedId = route.params.id
}
}
7. 高级应用场景
7.1 路由权限控制实现
基于路由元信息的权限控制方案:
javascript复制// router.js
const routes = [
{
path: '/admin',
component: () => import('./Admin.vue'),
meta: { requiresAuth: true }
}
]
// 全局前置守卫
router.beforeEach((to) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
return '/login'
}
})
在组合式 API 中的局部权限校验:
javascript复制import { onBeforeMount } from 'vue'
export default {
setup() {
const route = useRoute()
onBeforeMount(() => {
if (route.meta.requiresAdmin && !isAdmin()) {
throw new Error('无权访问')
}
})
}
}
7.2 基于路由的状态管理
将路由状态与 Pinia 结合:
javascript复制// stores/routeStore.js
import { defineStore } from 'pinia'
import { useRoute } from 'vue-router'
export const useRouteStore = defineStore('route', {
state: () => ({
currentQuery: null
}),
actions: {
updateQuery() {
const route = useRoute()
this.currentQuery = route.query
}
}
})
8. 性能优化技巧
8.1 路由懒加载的改进方案
传统懒加载写法:
javascript复制const routes = [
{
path: '/heavy',
component: () => import('./HeavyComponent.vue')
}
]
优化方案 - 添加加载状态和错误处理:
javascript复制const HeavyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200, // 默认200ms
timeout: 3000 // 3秒超时
})
8.2 路由预加载策略
在用户可能访问的路由上添加预加载:
javascript复制// 鼠标悬停时预加载
const preloadRoutes = ['/dashboard', '/settings']
preloadRoutes.forEach(path => {
router.preload(path)
})
// 或者在组件中使用
onMounted(() => {
router.preload('/next-page')
})
9. 测试相关实践
9.1 单元测试中的路由模拟
使用 vue-test-utils 测试路由相关组件:
javascript复制import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [...]
})
test('navigates on button click', async () => {
const wrapper = mount(Component, {
global: {
plugins: [router]
}
})
await wrapper.find('button').trigger('click')
expect(router.currentRoute.value.path).toBe('/target')
})
9.2 E2E 测试中的路由验证
使用 Cypress 测试路由跳转:
javascript复制describe('Navigation', () => {
it('should navigate to about page', () => {
cy.visit('/')
cy.get('[data-test="about-link"]').click()
cy.location('pathname').should('eq', '/about')
})
})
10. 与第三方库的集成
10.1 与 Pinia 的状态同步
在路由变化时自动更新 Pinia 状态:
javascript复制// stores/route.js
import { defineStore } from 'pinia'
import { useRoute } from 'vue-router'
export const useRouteStore = defineStore('route', {
state: () => ({
currentPath: ''
}),
actions: {
trackRoute() {
const route = useRoute()
watch(() => route.path, (path) => {
this.currentPath = path
}, { immediate: true })
}
}
})
10.2 与 i18n 的多语言路由
实现语言前缀的路由:
javascript复制const routes = [
{
path: '/:lang',
component: Layout,
children: [
{ path: 'home', component: Home }
]
}
]
router.beforeEach((to) => {
const lang = to.params.lang
if (lang) i18n.global.locale = lang
})
11. 项目结构组织建议
11.1 模块化路由定义
推荐的项目结构:
code复制src/
router/
index.js # 主路由配置
routes/
auth.js # 认证相关路由
admin.js # 管理后台路由
public.js # 公开路由
guards/ # 导航守卫
auth.js
permissions.js
11.2 动态路由注册方案
根据用户权限动态注册路由:
javascript复制// router.js
const router = createRouter({
history: createWebHistory(),
routes: getPublicRoutes()
})
// 登录后动态添加
function addDynamicRoutes(userRole) {
const dynamicRoutes = getRoutesForRole(userRole)
dynamicRoutes.forEach(route => {
router.addRoute(route)
})
}
12. 调试技巧与开发者工具
12.1 路由变化日志
开发环境下打印路由变化:
javascript复制router.afterEach((to, from) => {
if (import.meta.env.DEV) {
console.log(
`[路由] ${from.path} -> ${to.path}`,
`参数变化:`, JSON.stringify({
from: from.params,
to: to.params
})
)
}
})
12.2 Vue Devtools 中的路由信息
在 Vue Devtools 中:
- 切换到 "Routing" 标签页
- 查看当前路由的完整信息
- 可以手动触发导航跳转
- 查看路由匹配的组件树
13. 迁移指南:从 Vue 2 到 Vue 3
13.1 this.$router 的替代方案
Vue 2 选项式 API:
javascript复制methods: {
goBack() {
this.$router.go(-1)
}
}
Vue 3 组合式 API 等价写法:
javascript复制setup() {
const router = useRouter()
const goBack = () => {
router.go(-1)
}
return { goBack }
}
13.2 导航守卫的重写
Vue 2 全局守卫:
javascript复制router.beforeEach((to, from, next) => {
// ...
next()
})
Vue 3 中更推荐使用返回 Promise 的写法:
javascript复制router.beforeEach(async (to) => {
await checkAuth()
// 不需要显式调用 next()
})
14. 服务端渲染 (SSR) 特别注意事项
14.1 useRoute 在 SSR 中的使用限制
在服务端渲染时,useRoute() 只能访问到初始路由状态。解决方案:
javascript复制import { useRoute } from 'vue-router'
import { onServerPrefetch } from 'vue'
export default {
setup() {
const route = useRoute()
const fetchData = async () => {
// 使用 route.params.id 获取数据
}
onServerPrefetch(fetchData)
}
}
14.2 路由数据预取模式
实现 SSR 数据预取的两种方式:
- 组件内预取:
javascript复制export default {
async setup() {
const route = useRoute()
const data = ref(null)
const fetchData = async () => {
data.value = await fetch(`/api/${route.params.id}`)
}
if (import.meta.env.SSR) {
await fetchData()
} else {
onMounted(fetchData)
}
return { data }
}
}
- 路由配置预取:
javascript复制const routes = [
{
path: '/post/:id',
component: Post,
meta: {
ssrData: async (route) => {
return fetchPost(route.params.id)
}
}
}
]
15. 移动端特殊处理
15.1 路由过渡动画优化
针对移动端性能优化路由过渡:
vue复制<template>
<router-view v-slot="{ Component }">
<transition
name="fade-slide"
mode="out-in"
:duration="300"
>
<component :is="Component" />
</transition>
</router-view>
</template>
<style>
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.3s ease;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(30px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(-30px);
}
</style>
15.2 手势返回的实现
集成手势返回功能:
javascript复制import { useSwipe } from '@vueuse/core'
export default {
setup() {
const router = useRouter()
const { isSwiping, direction } = useSwipe(document.documentElement)
watch([isSwiping, direction], ([swiping, dir]) => {
if (swiping && dir === 'right') {
router.back()
}
})
}
}
16. 微前端集成方案
16.1 作为子应用的路由配置
在微前端架构中作为子应用时:
javascript复制let router = null
export async function mount(props) {
router = createRouter({
history: createWebHistory(props.baseUrl || '/'),
routes
})
app.use(router)
app.mount(props.container)
}
export async function unmount() {
router = null
}
16.2 主应用与子应用的路由隔离
确保主应用和子应用路由不冲突:
javascript复制// 子应用路由配置
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/subapp',
component: Layout,
children: [...]
}
]
})
17. 错误处理与监控
17.1 路由错误的全局捕获
统一处理导航错误:
javascript复制router.onError((error) => {
if (error.name === 'ChunkLoadError') {
// 处理组件加载失败
showError('加载失败,请刷新重试')
}
})
17.2 路由变化的性能监控
使用 Performance API 监控路由加载性能:
javascript复制router.beforeEach((to, from) => {
performance.mark('routeChangeStart')
})
router.afterEach(() => {
performance.mark('routeChangeEnd')
performance.measure(
'routeChange',
'routeChangeStart',
'routeChangeEnd'
)
const measures = performance.getEntriesByName('routeChange')
const lastMeasure = measures[measures.length - 1]
console.log(`路由切换耗时: ${lastMeasure.duration}ms`)
// 上报到监控系统
reportPerf('route_change', lastMeasure.duration)
})
18. 未来演进与替代方案
18.1 Vue Router 的未来路线
根据 Vue Router 团队的公开讨论,未来可能的方向包括:
- 更深入的组合式 API 集成
- 基于文件系统的路由(类似 Nuxt)
- 更好的懒加载控制
- 改进的滚动行为管理
18.2 其他路由方案对比
虽然 Vue Router 是官方推荐,但也有其他选择:
| 方案 | 特点 | 适用场景 |
|---|---|---|
| Vue Router | 官方维护,功能全面 | 大多数项目 |
| Vitepress | 基于文件的路由 | 文档站点 |
| Unplugin-vue-router | 文件系统路由 | 喜欢约定式路由的项目 |
| Handmade | 自定义简单实现 | 极简项目 |
在最近的一个项目中,我们尝试了 unplugin-vue-router,它通过文件系统自动生成路由配置,减少了手动维护路由表的工作量。但对于需要复杂路由配置的大型项目,Vue Router 仍然是更成熟的选择。
