Vue Router 教程

项目概述

Vue Router 是 Vue.js 的官方路由管理器,与 Vue.js 核心深度集成,提供了声明式的路由配置、嵌套路由、参数路由、导航守卫等功能。它使构建单页应用(SPA)变得更加简单和直观,能够轻松管理应用的导航状态和 URL 结构。

安装设置

1. 安装 Vue Router

Vue 3

# 使用 npm
npm install vue-router@4

# 使用 yarn
yarn add vue-router@4

# 使用 pnpm
pnpm add vue-router@4

Vue 2

# 使用 npm
npm install vue-router@3

# 使用 yarn
yarn add vue-router@3

# 使用 pnpm
pnpm add vue-router@3

2. 基本配置

Vue 3

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    // 懒加载组件
    component: () => import('../views/AboutView.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

// 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 2

// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

3. 应用模板

<!-- App.vue -->
<template>
  <div>
    <!-- 导航链接 -->
    <nav>
      <router-link to="/">首页</router-link> |
      <router-link to="/about">关于我们</router-link>
    </nav>
    
    <!-- 路由视图 -->
    <router-view />
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<!-- 或者使用 Composition API -->
<script setup>
// 组件逻辑
</script>

核心功能

1. 基本路由

路由配置

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/about',
    component: () => import('../views/AboutView.vue')
  },
  {
    path: '/contact',
    component: () => import('../views/ContactView.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

导航链接

<template>
  <nav>
    <!-- 基本链接 -->
    <router-link to="/">首页</router-link>
    
    <!-- 命名路由 -->
    <router-link :to="{ name: 'about' }">关于我们</router-link>
    
    <!-- 带查询参数 -->
    <router-link :to="{ path: '/search', query: { q: 'vue' } }">搜索 Vue</router-link>
    
    <!-- 带哈希 -->
    <router-link :to="{ path: '/about', hash: '#team' }">关于我们 - 团队</router-link>
  </nav>
  <router-view />
</template>

2. 嵌套路由

路由配置

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/user',
    component: () => import('../views/UserLayout.vue'),
    children: [
      {
        path: '', // 空路径,表示 /user 的默认子路由
        component: () => import('../views/UserProfile.vue')
      },
      {
        path: 'settings', // 完整路径: /user/settings
        component: () => import('../views/UserSettings.vue')
      },
      {
        path: 'posts', // 完整路径: /user/posts
        component: () => import('../views/UserPosts.vue')
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

布局组件

<!-- UserLayout.vue -->
<template>
  <div class="user-layout">
    <h1>用户中心</h1>
    <nav>
      <router-link to="/user">个人资料</router-link> |
      <router-link to="/user/settings">设置</router-link> |
      <router-link to="/user/posts">我的帖子</router-link>
    </nav>
    <div class="content">
      <!-- 子路由视图 -->
      <router-view />
    </div>
  </div>
</template>

3. 参数路由

路由配置

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/user/:id', // 动态参数
    component: () => import('../views/UserDetailView.vue'),
    props: true // 将路由参数作为 props 传递给组件
  },
  {
    path: '/product/:category/:id', // 多个动态参数
    component: () => import('../views/ProductDetailView.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

组件中获取参数

<!-- UserDetailView.vue -->
<template>
  <div>
    <h1>用户详情</h1>
    <p>用户 ID: {{ id }}</p>
  </div>
</template>

<!-- Option API -->
<script>
export default {
  props: ['id'], // 从路由参数获取
  mounted() {
    // 也可以通过 this.$route.params 获取
    console.log(this.$route.params.id)
  }
}
</script>

<!-- 或者 Composition API -->
<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()
const id = route.params.id

// 响应式获取参数
import { computed } from 'vue'
const userId = computed(() => route.params.id)
</script>

4. 编程式导航

使用 router.push

<template>
  <div>
    <button @click="navigateToHome">返回首页</button>
    <button @click="navigateToUser(1)">查看用户 1</button>
    <button @click="navigateToSearch('vue')">搜索 Vue</button>
  </div>
</template>

<!-- Option API -->
<script>
export default {
  methods: {
    navigateToHome() {
      this.$router.push('/')
    },
    navigateToUser(id) {
      // 通过路径
      this.$router.push(`/user/${id}`)
      
      // 通过命名路由
      this.$router.push({
        name: 'user',
        params: { id }
      })
    },
    navigateToSearch(query) {
      this.$router.push({
        path: '/search',
        query: { q: query }
      })
    }
  }
}
</script>

<!-- Composition API -->
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

function navigateToHome() {
  router.push('/')
}

function navigateToUser(id) {
  router.push(`/user/${id}`)
}

function navigateToSearch(query) {
  router.push({
    path: '/search',
    query: { q: query }
  })
}
</script>

其他导航方法

// 替换当前历史记录(不会添加新条目)
router.replace('/about')

// 后退一步
router.back()

// 前进一步
router.forward()

// 移动指定步数
router.go(-2) // 后退两步
router.go(1) // 前进一步

5. 重定向和别名

重定向

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/home',
    redirect: '/' // 重定向到首页
  },
  {
    path: '/old-user/:id',
    redirect: to => {
      // 动态重定向
      return {
        path: `/user/${to.params.id}`,
        query: to.query
      }
    }
  }
]

别名

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue'),
    alias: '/home' // 别名
  },
  {
    path: '/user/:id',
    component: () => import('../views/UserDetailView.vue'),
    alias: ['/profile/:id', '/member/:id'] // 多个别名
  }
]

6. 404 页面

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  // 其他路由...
  {
    // 捕获所有未匹配的路由
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFoundView.vue')
  }
]
<!-- NotFoundView.vue -->
<template>
  <div>
    <h1>404</h1>
    <p>页面不存在</p>
    <router-link to="/">返回首页</router-link>
  </div>
</template>

高级功能

1. 导航守卫

全局前置守卫

// router/index.js
router.beforeEach((to, from, next) => {
  // 检查用户是否登录
  const isLoggedIn = localStorage.getItem('token')
  
  // 需要认证的路由
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  
  if (requiresAuth && !isLoggedIn) {
    // 未登录,重定向到登录页
    next({
      path: '/login',
      query: { redirect: to.fullPath } // 保存重定向路径
    })
  } else {
    // 允许导航
    next()
  }
})

路由配置中的元信息

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/login',
    component: () => import('../views/LoginView.vue')
  },
  {
    path: '/dashboard',
    component: () => import('../views/DashboardView.vue'),
    meta: { requiresAuth: true } // 需要认证
  }
]

全局后置守卫

// router/index.js
router.afterEach((to, from) => {
  // 页面标题设置
  document.title = to.meta.title || '我的应用'
  
  // 页面统计
  console.log(`从 ${from.path} 导航到 ${to.path}`)
})

路由独享守卫

// router/index.js
const routes = [
  {
    path: '/dashboard',
    component: () => import('../views/DashboardView.vue'),
    beforeEnter: (to, from, next) => {
      // 路由独享的守卫
      const isAdmin = localStorage.getItem('role') === 'admin'
      if (!isAdmin) {
        next('/')
      } else {
        next()
      }
    }
  }
]

组件内守卫

<!-- UserDetailView.vue -->
<template>
  <!-- 组件内容 -->
</template>

<script>
export default {
  // 进入组件前
  beforeRouteEnter(to, from, next) {
    // 此时组件实例还未创建
    console.log('进入用户详情页')
    next()
  },
  
  // 路由更新时(参数变化)
  beforeRouteUpdate(to, from, next) {
    // 组件实例已存在
    console.log('用户 ID 已更新:', to.params.id)
    this.loadUserData(to.params.id)
    next()
  },
  
  // 离开组件前
  beforeRouteLeave(to, from, next) {
    if (this.hasUnsavedChanges) {
      if (confirm('有未保存的更改,确定要离开吗?')) {
        next()
      } else {
        next(false) // 取消导航
      }
    } else {
      next()
    }
  },
  
  methods: {
    loadUserData(id) {
      // 加载用户数据
    }
  }
}
</script>

<!-- 或者 Composition API -->
<script setup>
import { onBeforeRouteEnter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'

onBeforeRouteEnter((to, from, next) => {
  console.log('进入用户详情页')
  next()
})

onBeforeRouteUpdate((to, from, next) => {
  console.log('用户 ID 已更新:', to.params.id)
  loadUserData(to.params.id)
  next()
})

onBeforeRouteLeave((to, from, next) => {
  const hasUnsavedChanges = false // 检查是否有未保存的更改
  if (hasUnsavedChanges) {
    if (confirm('有未保存的更改,确定要离开吗?')) {
      next()
    } else {
      next(false)
    }
  } else {
    next()
  }
})

function loadUserData(id) {
  // 加载用户数据
}
</script>

2. 路由懒加载

基本懒加载

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/about',
    component: () => import('../views/AboutView.vue')
  }
]

带命名的懒加载

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import(/* webpackChunkName: "home" */ '../views/HomeView.vue')
  },
  {
    path: '/about',
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  }
]

3. 滚动行为

// router/index.js
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 如果有保存的位置,恢复它
    if (savedPosition) {
      return savedPosition
    }
    
    // 否则滚动到顶部
    return { top: 0 }
    
    // 或者滚动到指定元素
    // if (to.hash) {
    //   return {
    //     selector: to.hash
    //   }
    // }
  }
})

4. 路由元信息

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue'),
    meta: {
      title: '首页',
      requiresAuth: false
    }
  },
  {
    path: '/dashboard',
    component: () => import('../views/DashboardView.vue'),
    meta: {
      title: '仪表盘',
      requiresAuth: true,
      requiresAdmin: true
    }
  }
]

// 全局后置守卫设置标题
router.afterEach((to) => {
  document.title = to.meta.title || '我的应用'
})

// 全局前置守卫检查权限
router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token')
  const isAdmin = localStorage.getItem('role') === 'admin'
  
  if (to.meta.requiresAuth && !isLoggedIn) {
    next('/login')
  } else if (to.meta.requiresAdmin && !isAdmin) {
    next('/')
  } else {
    next()
  }
})

5. 动态路由

添加路由

// 动态添加路由
router.addRoute({
  path: '/new-route',
  component: () => import('../views/NewRouteView.vue')
})

// 添加嵌套路由
router.addRoute('parent', {
  path: 'child',
  component: () => import('../views/ChildView.vue')
})

删除路由

// 通过名称删除路由
router.removeRoute('routeName')

// 通过添加一个同名路由来替换
router.addRoute({ path: '/old', name: 'routeName', component: OldComponent })
router.addRoute({ path: '/new', name: 'routeName', component: NewComponent }) // 替换

实际应用场景

1. 认证系统

路由配置

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/login',
    component: () => import('../views/LoginView.vue'),
    meta: {
      requiresGuest: true
    }
  },
  {
    path: '/register',
    component: () => import('../views/RegisterView.vue'),
    meta: {
      requiresGuest: true
    }
  },
  {
    path: '/dashboard',
    component: () => import('../views/DashboardView.vue'),
    meta: {
      requiresAuth: true
    }
  },
  {
    path: '/profile',
    component: () => import('../views/ProfileView.vue'),
    meta: {
      requiresAuth: true
    }
  }
]

导航守卫

// router/index.js
router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token')
  
  // 检查是否需要认证
  if (to.meta.requiresAuth && !isLoggedIn) {
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  }
  // 检查是否需要访客状态
  else if (to.meta.requiresGuest && isLoggedIn) {
    next('/dashboard')
  }
  else {
    next()
  }
})

登录组件

<!-- LoginView.vue -->
<template>
  <div>
    <h1>登录</h1>
    <form @submit.prevent="handleLogin">
      <input type="text" v-model="username" placeholder="用户名" />
      <input type="password" v-model="password" placeholder="密码" />
      <button type="submit">登录</button>
    </form>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()
const username = ref('')
const password = ref('')

const handleLogin = async () => {
  try {
    // 模拟登录
    const token = 'fake-token'
    localStorage.setItem('token', token)
    
    // 登录成功后重定向
    const redirectPath = route.query.redirect || '/dashboard'
    router.push(redirectPath)
  } catch (error) {
    console.error('登录失败:', error)
  }
}
</script>

2. 电商应用

路由配置

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/products',
    component: () => import('../views/ProductsView.vue')
  },
  {
    path: '/product/:id',
    component: () => import('../views/ProductDetailView.vue'),
    props: true
  },
  {
    path: '/cart',
    component: () => import('../views/CartView.vue')
  },
  {
    path: '/checkout',
    component: () => import('../views/CheckoutView.vue'),
    meta: {
      requiresAuth: true
    }
  },
  {
    path: '/order/:id',
    component: () => import('../views/OrderDetailView.vue'),
    meta: {
      requiresAuth: true
    }
  }
]

商品详情组件

<!-- ProductDetailView.vue -->
<template>
  <div>
    <h1>{{ product.name }}</h1>
    <p>{{ product.description }}</p>
    <p>价格: ¥{{ product.price }}</p>
    <button @click="addToCart">加入购物车</button>
    <router-link to="/products">返回商品列表</router-link>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()
const product = ref({})

const id = route.params.id

onMounted(() => {
  // 加载商品数据
  fetchProduct(id)
})

const fetchProduct = async (productId) => {
  // 模拟 API 请求
  product.value = {
    id: productId,
    name: '示例商品',
description: '这是一个示例商品',
    price: 99.99
  }
}

const addToCart = () => {
  // 加入购物车逻辑
  console.log('加入购物车:', product.value)
  router.push('/cart')
}
</script>

3. 管理后台

路由配置

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/admin',
    component: () => import('../views/AdminLayout.vue'),
    meta: {
      requiresAuth: true,
      requiresAdmin: true
    },
    children: [
      {
        path: '',
        component: () => import('../views/AdminDashboardView.vue')
      },
      {
        path: 'users',
        component: () => import('../views/AdminUsersView.vue')
      },
      {
        path: 'products',
        component: () => import('../views/AdminProductsView.vue')
      },
      {
        path: 'orders',
        component: () => import('../views/AdminOrdersView.vue')
      }
    ]
  }
]

管理后台布局

<!-- AdminLayout.vue -->
<template>
  <div class="admin-layout">
    <aside>
      <h2>管理后台</h2>
      <nav>
        <router-link to="/admin">仪表盘</router-link>
        <router-link to="/admin/users">用户管理</router-link>
        <router-link to="/admin/products">商品管理</router-link>
        <router-link to="/admin/orders">订单管理</router-link>
      </nav>
    </aside>
    <main>
      <router-view />
    </main>
  </div>
</template>

代码优化建议

  1. 路由配置分离:将路由配置分离到单独的文件中,按功能模块组织
  2. 使用懒加载:对所有路由组件使用懒加载,减小初始包大小
  3. 导航守卫抽象:将认证逻辑抽象为可重用的导航守卫
  4. 嵌套路由合理使用:利用嵌套路由和布局组件减少代码重复
  5. 参数验证:对路由参数进行验证,确保数据安全性
  6. 错误处理:添加 404 页面和路由错误处理
  7. 性能优化
    • 使用适当的缓存策略
    • 避免在路由守卫中进行昂贵的计算
    • 合理使用 scrollBehavior 提升用户体验
  8. 代码组织
    • 按功能模块组织路由和组件
    • 使用一致的命名规范
    • 添加适当的注释
  9. SEO 考虑:对于需要 SEO 的页面,考虑使用服务端渲染或预渲染
  10. 测试:为路由配置和导航逻辑添加测试

参考资源

« 上一篇 React Router 教程 - React 的声明式路由库 下一篇 » React Router DOM 教程 - React Router 的 DOM 绑定