Skip to content

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 反馈。

vitepress开发指南