Vue Router 路由
Vue Router 是 Vue.js 官方路由管理器的全面指南。
Vue Router 简介
Vue Router 是 Vue.js 的官方路由库。它与 Vue.js 核心深度集成,使构建单页应用 (SPA) 变得轻而易举。主要特点包括:
- 嵌套路由/视图映射
- 模块化、基于组件的路由配置
- 路由参数、查询、通配符
- 由 Vue.js 过渡系统提供支持的视图过渡效果
- 细粒度的导航控制
- 带有自动激活 CSS 类的链接
- HTML5 历史模式或哈希模式
- 可定制的滚动行为
- URL 的正确编码
安装
直接下载 / CDN
html
<script src="https://unpkg.com/vue-router@4"></script>
NPM
bash
npm install vue-router@4
Yarn
bash
yarn add vue-router@4
基本用法
创建路由实例
javascript
// 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
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
与 Vue 应用程序集成
javascript
// 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')
在组件中使用路由
vue
<!-- App.vue -->
<template>
<div id="app">
<nav>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于</router-link>
</nav>
<router-view/>
</div>
</template>
核心概念
动态路由匹配
javascript
const routes = [
// 动态路径参数以冒号开头
{ path: '/user/:id', component: User }
]
访问路由参数:
vue
<template>
<div>用户 {{ $route.params.id }}</div>
</template>
<!-- 或者使用组合式 API -->
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id)
</script>
嵌套路由
javascript
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功
// UserProfile 将被渲染在 User 的 <router-view> 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 将被渲染在 User 的 <router-view> 中
path: 'posts',
component: UserPosts
}
]
}
]
编程式导航
javascript
// 在组件方法中
methods: {
goToHome() {
this.$router.push('/')
},
goToUser(userId) {
this.$router.push(`/user/${userId}`)
},
goBack() {
this.$router.go(-1)
}
}
// 使用组合式 API
import { useRouter } from 'vue-router'
const router = useRouter()
function goToHome() {
router.push('/')
}
命名路由
javascript
const routes = [
{
path: '/user/:id',
name: 'user',
component: User
}
]
// 导航到命名路由
router.push({ name: 'user', params: { id: '123' } })
命名视图
vue
<template>
<div>
<router-view name="header"></router-view>
<router-view></router-view>
<router-view name="footer"></router-view>
</div>
</template>
javascript
const routes = [
{
path: '/',
components: {
default: Home,
header: Header,
footer: Footer
}
}
]
高级功能
导航守卫
全局守卫
javascript
const router = createRouter({ ... })
// 全局前置守卫
router.beforeEach((to, from) => {
// 检查用户是否已登录
const isAuthenticated = checkAuth()
// 如果未认证且尝试访问受限页面
if (!isAuthenticated && to.meta.requiresAuth) {
// 重定向到登录页面
return { name: 'Login' }
}
})
// 全局后置钩子
router.afterEach((to, from) => {
// 发送分析数据
sendAnalytics(to.fullPath)
})
路由独享守卫
javascript
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from) => {
// 检查用户是否是管理员
if (!isAdmin()) {
return false
}
}
}
]
组件内守卫
vue
<script>
export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this`
next(vm => {
// 通过 `vm` 访问组件实例
})
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
// 示例:如果表单未保存,阻止离开
if (this.formIsDirty) {
if (!window.confirm('放弃更改?')) {
return false
}
}
}
}
</script>
路由元字段
javascript
const routes = [
{
path: '/admin',
component: Admin,
meta: {
requiresAuth: true,
title: '管理面板',
roles: ['admin', 'super-admin']
}
}
]
// 使用元字段
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(currentUserRole)) {
return { name: 'Forbidden' }
}
})
路由懒加载
javascript
// 不要同步导入
// import UserDetails from './views/UserDetails.vue'
// 使用动态导入实现懒加载
const routes = [
{
path: '/user/:id',
name: 'UserDetails',
component: () => import('./views/UserDetails.vue')
}
]
滚动行为
javascript
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 如果存在保存的位置,则返回
if (savedPosition) {
return savedPosition
}
// 对于新导航,滚动到顶部
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth'
}
}
// 始终滚动到顶部
return { top: 0 }
}
})
最佳实践
路由组织
对于大型应用程序,最好将路由组织成模块:
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import homeRoutes from './modules/home'
import userRoutes from './modules/user'
import productRoutes from './modules/product'
const routes = [
...homeRoutes,
...userRoutes,
...productRoutes,
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
// router/modules/user.js
export default [
{
path: '/users',
component: () => import('@/views/users/UserList.vue'),
children: [
{
path: ':id',
component: () => import('@/views/users/UserDetails.vue')
}
]
}
]
认证和授权
javascript
// router/index.js
const router = createRouter({ ... })
router.beforeEach(async (to, from) => {
// 检查路由是否需要认证
if (to.matched.some(record => record.meta.requiresAuth)) {
// 检查用户是否已登录
const isLoggedIn = await checkUserSession()
if (!isLoggedIn) {
// 保存我们尝试访问的位置
return {
name: 'Login',
query: { redirect: to.fullPath }
}
}
// 检查所需权限
if (to.meta.requiredPermissions) {
const userPermissions = await getUserPermissions()
// 检查用户是否拥有所有所需权限
const hasPermission = to.meta.requiredPermissions.every(
permission => userPermissions.includes(permission)
)
if (!hasPermission) {
return { name: 'Forbidden' }
}
}
}
})
// 登录成功后,重定向到保存的位置
function handleLoginSuccess() {
const redirectPath = router.currentRoute.value.query.redirect || '/'
router.push(redirectPath)
}
路由过渡
vue
<!-- App.vue -->
<template>
<div id="app">
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
</div>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
基于路由的代码分割
javascript
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
// 使用 webpackChunkName 分组相关块
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue'),
children: [
{
path: 'analytics',
name: 'Analytics',
component: () => import(/* webpackChunkName: "dashboard" */ './views/Analytics.vue')
},
{
path: 'settings',
name: 'Settings',
component: () => import(/* webpackChunkName: "dashboard" */ './views/Settings.vue')
}
]
}
]
在组合式 API 中使用 Vue Router
使用 Router 和 Route
vue
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 访问路由参数
console.log(route.params.id)
// 编程式导航
function navigateTo(id) {
router.push({ name: 'UserDetails', params: { id } })
}
// 监听路由变化
import { watch } from 'vue'
watch(
() => route.params,
(newParams) => {
console.log('路由参数变化:', newParams)
// 基于参数获取新数据
}
)
</script>
自定义路由组合式函数
javascript
// composables/useAuth.js
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
export function useAuth() {
const router = useRouter()
const route = useRoute()
const isAuthenticated = computed(() => {
return !!localStorage.getItem('token')
})
function login(credentials) {
// 登录逻辑
return api.login(credentials).then(response => {
localStorage.setItem('token', response.token)
// 重定向到原始目的地或首页
const redirectPath = route.query.redirect || '/'
router.push(redirectPath)
})
}
function logout() {
localStorage.removeItem('token')
router.push('/login')
}
return {
isAuthenticated,
login,
logout
}
}
// 在组件中使用
import { useAuth } from '@/composables/useAuth'
const { isAuthenticated, login, logout } = useAuth()
测试 Vue Router
使用 Vue Test Utils 测试
javascript
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import App from '@/App.vue'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
// 创建用于测试的路由实例
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
const router = createRouter({
history: createWebHistory(),
routes
})
test('路由测试', async () => {
router.push('/')
await router.isReady()
const wrapper = mount(App, {
global: {
plugins: [router]
}
})
expect(wrapper.html()).toContain('首页')
// 导航到关于页面
await wrapper.find('a[href="/about"]').trigger('click')
// 等待路由变化
await router.isReady()
// 检查是否渲染了关于页面
expect(wrapper.html()).toContain('关于页面')
})
测试导航守卫
javascript
import { beforeEach } from '@/router'
// 模拟依赖
jest.mock('@/services/auth', () => ({
isAuthenticated: jest.fn()
}))
import { isAuthenticated } from '@/services/auth'
describe('路由守卫', () => {
test('未认证访问受保护路由时重定向到登录', () => {
// 设置
isAuthenticated.mockReturnValue(false)
const to = {
matched: [{ meta: { requiresAuth: true } }],
fullPath: '/dashboard'
}
const from = {}
// 执行守卫
const result = beforeEach(to, from)
// 断言
expect(result).toEqual({
name: 'Login',
query: { redirect: '/dashboard' }
})
})
test('已认证时允许访问受保护路由', () => {
// 设置
isAuthenticated.mockReturnValue(true)
const to = {
matched: [{ meta: { requiresAuth: true } }]
}
const from = {}
// 执行守卫
const result = beforeEach(to, from)
// 断言
expect(result).toBeUndefined()
})
})
常见问题解答
如何处理 404 页面?
javascript
const routes = [
// 其他路由...
// 这将匹配任何未被先前路由匹配的路径
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}
]
如何处理路由过渡?
在 router-view 周围使用 Vue 的 transition 组件:
vue
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
如何向路由组件传递 props?
javascript
const routes = [
{
path: '/user/:id',
component: User,
// 启用路由的 props
props: true
},
{
path: '/search',
component: SearchResults,
// 函数模式允许转换参数
props: route => ({ query: route.query.q })
}
]
// 在组件中
export default {
props: ['id'] // 或 props: ['query']
}
如何处理基于路由的权限?
使用元字段和导航守卫:
javascript
const routes = [
{
path: '/admin',
component: Admin,
meta: { requiredRole: 'admin' }
}
]
router.beforeEach((to, from) => {
if (to.meta.requiredRole && userRole !== to.meta.requiredRole) {
return { name: 'Forbidden' }
}
})
如何处理查询参数?
javascript
// 带查询参数导航
router.push({ path: '/search', query: { q: 'vue router' } })
// 访问查询参数
const query = route.query.q
相关资源
本文档将持续更新,如有问题请通过 GitHub Issues 反馈。