在 Vue 3 的组合式 API 设计中,路由管理是构建单页应用(SPA)的关键环节。传统选项式 API 中,我们通过 this.$router 和 this.$route 访问路由实例和当前路由信息,但在组合式 API 中,这种基于组件实例的访问方式不再适用。Vue Router 4.x 为此专门提供了 useRouter 和 useRoute 这两个组合式函数,它们代表了现代 Vue 开发中路由操作的最佳实践。
我曾在一个电商后台管理系统的重构中,将原有基于选项式 API 的路由逻辑全部迁移到组合式 API。实测发现,使用 useRouter 进行编程式导航时,代码组织更加清晰,路由跳转与组件逻辑的耦合度显著降低。特别是在处理复杂权限校验流程时,组合式函数的表现尤为出色。
在 setup 函数或 <script setup> 中引入 useRouter 非常简单:
javascript复制import { useRouter } from 'vue-router'
// 在 setup 中
const router = useRouter()
这个 router 实例等同于选项式 API 中的 this.$router,但有几个关键区别需要注意:
<script setup> 中使用javascript复制// 1. 字符串路径
router.push('/users')
// 2. 带参数的对象形式
router.push({
path: '/users',
query: { page: 2 }
})
// 3. 命名路由(推荐)
router.push({
name: 'userProfile',
params: { userId: 123 }
})
重要提示:当使用 params 时,必须提供 name 而非 path。这是新手常犯的错误,我在实际项目中曾因此浪费两小时排查问题。
我们可以在组合式 API 中创建全局守卫:
javascript复制router.beforeEach((to, from) => {
// 返回 false 取消导航
if (!isAuthenticated()) return '/login'
})
更妙的是,现在可以在 setup 中直接使用守卫逻辑:
javascript复制import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
return confirm('确定要离开吗?')
}
})
javascript复制const router = createRouter({
routes: [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
// 预加载配置(Vue Router 4.1+)
meta: { preload: true }
}
]
})
javascript复制const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else if (to.hash) {
return { el: to.hash }
} else {
return { top: 0 }
}
}
})
useRoute 提供了当前路由的响应式访问:
javascript复制import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.path) // 当前路径
console.log(route.params.id) // 动态参数
console.log(route.query.q) // 查询参数
与选项式 API 不同,这里的 route 对象是完全响应式的。这意味着你可以直接在模板中使用它,或通过 watch 监听路由变化。
javascript复制watch(
() => route.params.id,
(newId) => {
fetchUserDetails(newId)
}
)
javascript复制const userId = computed(() => route.params.id)
javascript复制const page = computed({
get: () => parseInt(route.query.page) || 1,
set: (val) => {
router.push({ query: { ...route.query, page: val } })
}
})
在路由配置中定义 meta 字段:
javascript复制{
path: '/admin',
meta: { requiresAuth: true }
}
在组件中访问:
javascript复制const requiresAuth = computed(() => route.meta.requiresAuth)
将常用路由操作封装成可复用的组合式函数:
javascript复制// useNavigation.js
export function useNavigation() {
const router = useRouter()
const navigateToUser = (userId) => {
router.push({ name: 'user', params: { userId } })
}
return { navigateToUser }
}
为路由添加类型定义:
typescript复制declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
title?: string
}
}
javascript复制// 在 router.push 调用处添加 catch 处理
router.push('/path').catch(err => {
if (!err.message.includes('Avoided redundant navigation')) {
console.error(err)
}
})
解决方案1:使用 key 强制重新渲染
vue复制<router-view :key="route.fullPath" />
解决方案2:手动监听变化
javascript复制watch(
() => route.params.id,
() => { /* 处理逻辑 */ }
)
推荐使用 Suspense + async setup:
vue复制<script setup>
const { data } = await useFetchData(route.params.id)
</script>
基于路由守卫的权限控制流程:
javascript复制router.beforeEach(async (to) => {
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return {
path: '/login',
query: { redirect: to.fullPath }
}
}
if (to.meta.roles && !hasRequiredRoles(to.meta.roles)) {
return '/403'
}
})
项目目录结构示例:
code复制src/
router/
index.js # 主配置
routes/
auth.js # 认证相关路由
admin.js # 管理后台路由
public.js # 公开路由
处理主应用与子应用的路由协同:
javascript复制// 主应用路由配置
{
path: '/app1/*',
component: MicroAppContainer,
meta: { microApp: 'app1' }
}
使用 webpack 魔法注释优化 chunk 名称:
javascript复制component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue')
实现全局加载指示器:
javascript复制router.beforeEach(() => {
loadingBar.start()
})
router.afterEach(() => {
loadingBar.finish()
})
启用路由历史记录:
javascript复制const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
// 开发环境下记录路由栈
devtools: process.env.NODE_ENV === 'development'
})
在控制台快速访问路由信息:
javascript复制// 在组件中临时暴露
const router = useRouter()
const route = useRoute()
window.__router = router
window.__route = route
使用 vue-test-utils 测试路由相关组件:
javascript复制import { mount } from '@vue/test-utils'
test('navigates to login when not authenticated', async () => {
const push = jest.fn()
useRouter.mockImplementation(() => ({ push }))
const wrapper = mount(ProtectedComponent)
await wrapper.find('button').trigger('click')
expect(push).toHaveBeenCalledWith('/login')
})
使用 Cypress 进行路由测试:
javascript复制describe('Navigation', () => {
it('should redirect to login when accessing protected route', () => {
cy.visit('/dashboard')
cy.url().should('include', '/login')
})
})
在 Pinia store 中响应路由变化:
javascript复制export const useUserStore = defineStore('user', {
actions: {
async loadUser() {
const route = useRoute()
this.user = await fetchUser(route.params.userId)
}
}
})
javascript复制const cachedData = computed(() => {
const route = useRoute()
return cache[route.fullPath]
})
| 选项式 API | 组合式 API 等效实现 |
|---|---|
| this.$router.push() | useRouter().push() |
| this.$route.params.id | useRoute().params.id |
| beforeRouteEnter 守卫 | onBeforeRouteEnter 组合式函数 |
Vue Router 团队正在探索的方向:
社区流行的路由相关库: