Skip to content

示例

探索 Vue Router 的实际示例和真实世界用例,学习最佳实践和高级模式。

基础示例

简单路由

展示如何设置 Vue Router 的基本示例:

vue
<template>
  <div id="app">
    <nav>
      <router-link to="/">首页</router-link>
      <router-link to="/about">关于</router-link>
      <router-link to="/contact">联系我们</router-link>
    </nav>
    <router-view />
  </div>
</template>

<script>
import { createRouter, createWebHistory } from 'vue-router'
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contact from './components/Contact.vue'

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/contact', component: Contact }
]

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

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

动态路由

使用动态路由参数的示例:

vue
<template>
  <div>
    <h1>用户资料</h1>
    <p>用户 ID: {{ $route.params.id }}</p>
    <p>用户名: {{ user.name }}</p>
  </div>
</template>

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

const route = useRoute()
const user = ref({})

// 监听路由参数变化
watch(() => route.params.id, async (newId) => {
  if (newId) {
    user.value = await fetchUser(newId)
  }
}, { immediate: true })

async function fetchUser(id) {
  // 模拟 API 调用
  return { id, name: `用户 ${id}` }
}
</script>

路由配置:

javascript
const routes = [
  { path: '/user/:id', component: UserProfile }
]

中级示例

嵌套路由

多层嵌套路由的示例:

vue
<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <router-link to="/user/123">用户资料</router-link>
      <router-link to="/user/123/posts">用户文章</router-link>
      <router-link to="/user/123/settings">用户设置</router-link>
    </nav>
    <router-view />
  </div>
</template>

<!-- User.vue -->
<template>
  <div class="user">
    <h2>用户 {{ $route.params.id }}</h2>
    <nav class="user-nav">
      <router-link :to="`/user/${$route.params.id}`">资料</router-link>
      <router-link :to="`/user/${$route.params.id}/posts`">文章</router-link>
      <router-link :to="`/user/${$route.params.id}/settings`">设置</router-link>
    </nav>
    <router-view />
  </div>
</template>

<!-- UserProfile.vue -->
<template>
  <div class="user-profile">
    <h3>个人信息</h3>
    <p>姓名: {{ user.name }}</p>
    <p>邮箱: {{ user.email }}</p>
  </div>
</template>

<!-- UserPosts.vue -->
<template>
  <div class="user-posts">
    <h3>用户文章</h3>
    <div v-for="post in posts" :key="post.id" class="post">
      <h4>{{ post.title }}</h4>
      <p>{{ post.content }}</p>
    </div>
  </div>
</template>

路由配置:

javascript
const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      { path: '', component: UserProfile },
      { path: 'posts', component: UserPosts },
      { path: 'settings', component: UserSettings }
    ]
  }
]

命名路由和导航

使用命名路由进行更清晰导航的示例:

vue
<template>
  <div>
    <h1>产品目录</h1>
    <div v-for="product in products" :key="product.id" class="product-card">
      <h3>{{ product.name }}</h3>
      <p>{{ product.description }}</p>
      <router-link 
        :to="{ name: 'product-detail', params: { id: product.id } }"
        class="btn"
      >
        查看详情
      </router-link>
    </div>
  </div>
</template>

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

const router = useRouter()
const products = [
  { id: 1, name: '笔记本电脑', description: '高性能笔记本电脑' },
  { id: 2, name: '智能手机', description: '最新款智能手机' }
]

function navigateToProduct(productId) {
  router.push({ 
    name: 'product-detail', 
    params: { id: productId },
    query: { tab: 'specifications' }
  })
}
</script>

路由配置:

javascript
const routes = [
  { 
    path: '/products', 
    name: 'products', 
    component: ProductList 
  },
  { 
    path: '/product/:id', 
    name: 'product-detail', 
    component: ProductDetail 
  }
]

高级示例

路由守卫和身份验证

实现身份验证的路由守卫示例:

javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const routes = [
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/Login.vue')
  },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/admin',
    name: 'admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true, requiresAdmin: true }
  }
]

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

// 全局导航守卫
router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()
  
  // 检查路由是否需要身份验证
  if (to.meta.requiresAuth) {
    if (!authStore.isAuthenticated) {
      next({ name: 'login', query: { redirect: to.fullPath } })
      return
    }
    
    // 检查管理员权限
    if (to.meta.requiresAdmin && !authStore.isAdmin) {
      next({ name: 'dashboard' })
      return
    }
  }
  
  next()
})

export default router
vue
<!-- Login.vue -->
<template>
  <div class="login-form">
    <h2>登录</h2>
    <form @submit.prevent="handleLogin">
      <input 
        v-model="credentials.username" 
        type="text" 
        placeholder="用户名"
        required
      />
      <input 
        v-model="credentials.password" 
        type="password" 
        placeholder="密码"
        required
      />
      <button type="submit" :disabled="loading">
        {{ loading ? '登录中...' : '登录' }}
      </button>
    </form>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()

const credentials = ref({
  username: '',
  password: ''
})
const loading = ref(false)

async function handleLogin() {
  loading.value = true
  
  try {
    await authStore.login(credentials.value)
    
    // 重定向到预期页面或仪表板
    const redirect = route.query.redirect || '/dashboard'
    router.push(redirect)
  } catch (error) {
    alert('登录失败: ' + error.message)
  } finally {
    loading.value = false
  }
}
</script>

数据获取模式

展示不同数据获取策略的示例:

vue
<!-- UserProfile.vue - 导航后获取 -->
<template>
  <div class="user-profile">
    <div v-if="loading" class="loading">
      正在加载用户资料...
    </div>
    
    <div v-else-if="error" class="error">
      <p>加载用户资料失败</p>
      <button @click="fetchUser">重试</button>
    </div>
    
    <div v-else-if="user" class="profile-content">
      <img :src="user.avatar" :alt="user.name" />
      <h1>{{ user.name }}</h1>
      <p>{{ user.bio }}</p>
      <div class="stats">
        <span>{{ user.posts }} 篇文章</span>
        <span>{{ user.followers }} 关注者</span>
      </div>
    </div>
  </div>
</template>

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

const route = useRoute()
const user = ref(null)
const loading = ref(false)
const error = ref(null)

// 监听路由参数并获取数据
watch(() => route.params.id, fetchUser, { immediate: true })

async function fetchUser(userId = route.params.id) {
  if (!userId) return
  
  loading.value = true
  error.value = null
  user.value = null
  
  try {
    const response = await fetch(`/api/users/${userId}`)
    if (!response.ok) throw new Error('获取用户失败')
    user.value = await response.json()
  } catch (err) {
    error.value = err
  } finally {
    loading.value = false
  }
}
</script>
vue
<!-- ProductDetail.vue - 导航前获取 -->
<script>
export default {
  async beforeRouteEnter(to, from, next) {
    try {
      const product = await fetchProduct(to.params.id)
      next(vm => vm.setProduct(product))
    } catch (error) {
      next(vm => vm.setError(error))
    }
  },
  
  async beforeRouteUpdate(to, from) {
    if (to.params.id !== from.params.id) {
      this.loading = true
      try {
        this.product = await fetchProduct(to.params.id)
        this.error = null
      } catch (error) {
        this.error = error
      } finally {
        this.loading = false
      }
    }
  },
  
  data() {
    return {
      product: null,
      loading: false,
      error: null
    }
  },
  
  methods: {
    setProduct(product) {
      this.product = product
    },
    
    setError(error) {
      this.error = error
    }
  }
}

async function fetchProduct(id) {
  const response = await fetch(`/api/products/${id}`)
  if (!response.ok) throw new Error('产品未找到')
  return response.json()
}
</script>

动态路由管理

动态添加和删除路由的示例:

vue
<!-- AdminPanel.vue -->
<template>
  <div class="admin-panel">
    <h2>管理面板</h2>
    
    <div class="plugin-management">
      <h3>插件管理</h3>
      <div v-for="plugin in availablePlugins" :key="plugin.id" class="plugin-item">
        <span>{{ plugin.name }}</span>
        <button 
          v-if="!plugin.installed" 
          @click="installPlugin(plugin)"
        >
          安装
        </button>
        <button 
          v-else 
          @click="uninstallPlugin(plugin)"
        >
          卸载
        </button>
      </div>
    </div>
    
    <div class="current-routes">
      <h3>当前路由</h3>
      <ul>
        <li v-for="route in currentRoutes" :key="route.path">
          {{ route.path }} - {{ route.name }}
        </li>
      </ul>
    </div>
  </div>
</template>

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

const router = useRouter()
const availablePlugins = ref([
  { 
    id: 'analytics', 
    name: '分析仪表板', 
    installed: false,
    routes: [
      {
        path: '/admin/analytics',
        name: 'analytics',
        component: () => import('@/plugins/analytics/Dashboard.vue')
      }
    ]
  },
  { 
    id: 'reports', 
    name: '报告模块', 
    installed: false,
    routes: [
      {
        path: '/admin/reports',
        name: 'reports',
        component: () => import('@/plugins/reports/Reports.vue')
      },
      {
        path: '/admin/reports/:id',
        name: 'report-detail',
        component: () => import('@/plugins/reports/ReportDetail.vue')
      }
    ]
  }
])

const currentRoutes = ref([])
const installedRoutes = new Map()

onMounted(() => {
  updateCurrentRoutes()
})

function installPlugin(plugin) {
  const removeCallbacks = []
  
  // 为此插件添加所有路由
  plugin.routes.forEach(route => {
    const removeRoute = router.addRoute('admin', route)
    removeCallbacks.push(removeRoute)
  })
  
  // 存储删除回调以便后续清理
  installedRoutes.set(plugin.id, removeCallbacks)
  
  plugin.installed = true
  updateCurrentRoutes()
  
  console.log(`插件 ${plugin.name} 已安装`)
}

function uninstallPlugin(plugin) {
  const removeCallbacks = installedRoutes.get(plugin.id)
  
  if (removeCallbacks) {
    // 删除此插件的所有路由
    removeCallbacks.forEach(removeRoute => removeRoute())
    installedRoutes.delete(plugin.id)
  }
  
  plugin.installed = false
  updateCurrentRoutes()
  
  console.log(`插件 ${plugin.name} 已卸载`)
}

function updateCurrentRoutes() {
  currentRoutes.value = router.getRoutes().map(route => ({
    path: route.path,
    name: route.name
  }))
}
</script>

滚动行为

实现自定义滚动行为的示例:

javascript
// router/index.js
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 如果有保存的位置(浏览器前进/后退),使用它
    if (savedPosition) {
      return savedPosition
    }
    
    // 如果导航到锚点,滚动到它
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth'
      }
    }
    
    // 对于某些路由,保持滚动位置
    if (to.meta.keepScrollPosition) {
      return false
    }
    
    // 默认:滚动到顶部
    return { top: 0, behavior: 'smooth' }
  }
})
vue
<!-- BlogPost.vue -->
<template>
  <article class="blog-post">
    <header>
      <h1>{{ post.title }}</h1>
      <nav class="table-of-contents">
        <h3>目录</h3>
        <ul>
          <li v-for="heading in tableOfContents" :key="heading.id">
            <router-link 
              :to="{ hash: `#${heading.id}` }"
              @click="scrollToHeading(heading.id)"
            >
              {{ heading.text }}
            </router-link>
          </li>
        </ul>
      </nav>
    </header>
    
    <div class="content" v-html="post.content"></div>
    
    <footer>
      <router-link 
        :to="{ name: 'blog-list' }"
        class="back-link"
      >
        ← 返回博客
      </router-link>
    </footer>
  </article>
</template>

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

const route = useRoute()
const post = ref({})
const tableOfContents = ref([])

onMounted(async () => {
  await loadPost()
  generateTableOfContents()
  
  // 如果存在哈希,滚动到它
  if (route.hash) {
    await nextTick()
    scrollToHeading(route.hash.slice(1))
  }
})

async function loadPost() {
  // 加载文章数据
  post.value = await fetchPost(route.params.id)
}

function generateTableOfContents() {
  // 从内容中提取标题
  const headings = post.value.content.match(/<h[2-6][^>]*>.*?<\/h[2-6]>/g) || []
  
  tableOfContents.value = headings.map((heading, index) => {
    const text = heading.replace(/<[^>]*>/g, '')
    const id = `heading-${index}`
    return { id, text }
  })
}

function scrollToHeading(id) {
  const element = document.getElementById(id)
  if (element) {
    element.scrollIntoView({ 
      behavior: 'smooth',
      block: 'start'
    })
  }
}
</script>

真实世界应用

电商应用

完整的电商应用结构示例:

javascript
// router/index.js
const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/products',
    name: 'products',
    component: () => import('@/views/ProductList.vue'),
    meta: { keepAlive: true }
  },
  {
    path: '/product/:id',
    name: 'product-detail',
    component: () => import('@/views/ProductDetail.vue'),
    props: true
  },
  {
    path: '/category/:slug',
    name: 'category',
    component: () => import('@/views/Category.vue'),
    props: true
  },
  {
    path: '/cart',
    name: 'cart',
    component: () => import('@/views/Cart.vue')
  },
  {
    path: '/checkout',
    name: 'checkout',
    component: () => import('@/views/Checkout.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/account',
    component: () => import('@/views/Account.vue'),
    meta: { requiresAuth: true },
    children: [
      {
        path: '',
        name: 'account-dashboard',
        component: () => import('@/views/account/Dashboard.vue')
      },
      {
        path: 'orders',
        name: 'account-orders',
        component: () => import('@/views/account/Orders.vue')
      },
      {
        path: 'orders/:id',
        name: 'order-detail',
        component: () => import('@/views/account/OrderDetail.vue'),
        props: true
      },
      {
        path: 'profile',
        name: 'account-profile',
        component: () => import('@/views/account/Profile.vue')
      }
    ]
  }
]

多租户应用

多租户 SaaS 应用的示例:

javascript
// router/index.js
const routes = [
  {
    path: '/',
    name: 'landing',
    component: () => import('@/views/Landing.vue')
  },
  {
    path: '/app/:tenant',
    component: () => import('@/layouts/TenantLayout.vue'),
    beforeEnter: validateTenant,
    children: [
      {
        path: '',
        name: 'tenant-dashboard',
        component: () => import('@/views/tenant/Dashboard.vue')
      },
      {
        path: 'users',
        name: 'tenant-users',
        component: () => import('@/views/tenant/Users.vue'),
        meta: { permission: 'users.read' }
      },
      {
        path: 'settings',
        name: 'tenant-settings',
        component: () => import('@/views/tenant/Settings.vue'),
        meta: { permission: 'settings.write' }
      }
    ]
  }
]

async function validateTenant(to, from, next) {
  const tenantSlug = to.params.tenant
  
  try {
    const tenant = await fetchTenant(tenantSlug)
    
    if (!tenant.active) {
      next({ name: 'tenant-suspended' })
      return
    }
    
    // 存储租户信息供组件使用
    to.meta.tenant = tenant
    next()
  } catch (error) {
    next({ name: 'tenant-not-found' })
  }
}

性能示例

懒加载和代码分割

javascript
// 懒加载组件
const routes = [
  {
    path: '/dashboard',
    component: () => import(
      /* webpackChunkName: "dashboard" */ 
      '@/views/Dashboard.vue'
    )
  },
  {
    path: '/reports',
    component: () => import(
      /* webpackChunkName: "reports" */ 
      '@/views/Reports.vue'
    )
  }
]

// 分组相关组件
const routes = [
  {
    path: '/admin',
    component: () => import(
      /* webpackChunkName: "admin" */ 
      '@/views/admin/Layout.vue'
    ),
    children: [
      {
        path: 'users',
        component: () => import(
          /* webpackChunkName: "admin" */ 
          '@/views/admin/Users.vue'
        )
      },
      {
        path: 'settings',
        component: () => import(
          /* webpackChunkName: "admin" */ 
          '@/views/admin/Settings.vue'
        )
      }
    ]
  }
]

基于路由的预取

vue
<template>
  <div class="product-grid">
    <div 
      v-for="product in products" 
      :key="product.id"
      class="product-card"
      @mouseenter="prefetchProduct(product.id)"
    >
      <router-link :to="`/product/${product.id}`">
        <img :src="product.image" :alt="product.name" />
        <h3>{{ product.name }}</h3>
        <p>¥{{ product.price }}</p>
      </router-link>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const products = ref([])
const prefetchedProducts = new Set()

async function prefetchProduct(productId) {
  if (prefetchedProducts.has(productId)) return
  
  prefetchedProducts.add(productId)
  
  // 预取产品数据
  try {
    await import(`@/views/ProductDetail.vue`)
    await fetch(`/api/products/${productId}`)
  } catch (error) {
    console.warn('预取失败:', error)
  }
}
</script>

测试示例

单元测试路由

javascript
// tests/router.test.js
import { mount } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import App from '@/App.vue'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

describe('Router', () => {
  let router

  beforeEach(async () => {
    router = createRouter({
      history: createMemoryHistory(),
      routes: [
        { path: '/', component: Home },
        { path: '/about', component: About }
      ]
    })

    await router.push('/')
  })

  it('渲染首页', async () => {
    const wrapper = mount(App, {
      global: {
        plugins: [router]
      }
    })

    expect(wrapper.findComponent(Home).exists()).toBe(true)
  })

  it('导航到关于页面', async () => {
    await router.push('/about')
    
    const wrapper = mount(App, {
      global: {
        plugins: [router]
      }
    })

    expect(wrapper.findComponent(About).exists()).toBe(true)
  })
})

使用 Cypress 进行 E2E 测试

javascript
// cypress/integration/navigation.spec.js
describe('导航', () => {
  beforeEach(() => {
    cy.visit('/')
  })

  it('在主要部分之间导航', () => {
    // 测试首页
    cy.contains('欢迎使用 Vue Router')
    cy.url().should('eq', Cypress.config().baseUrl + '/')

    // 导航到产品页面
    cy.get('[data-testid="nav-products"]').click()
    cy.url().should('include', '/products')
    cy.contains('产品目录')

    // 导航到特定产品
    cy.get('[data-testid="product-1"]').click()
    cy.url().should('include', '/product/1')
    cy.contains('产品详情')

    // 测试浏览器后退按钮
    cy.go('back')
    cy.url().should('include', '/products')
  })

  it('处理身份验证流程', () => {
    // 尝试访问受保护的路由
    cy.visit('/dashboard')
    cy.url().should('include', '/login')

    // 登录
    cy.get('[data-testid="username"]').type('testuser')
    cy.get('[data-testid="password"]').type('password')
    cy.get('[data-testid="login-button"]').click()

    // 应该重定向到仪表板
    cy.url().should('include', '/dashboard')
    cy.contains('欢迎来到仪表板')
  })
})

最佳实践总结

  1. 路由组织: 分组相关路由,使用嵌套路由处理层次结构
  2. 懒加载: 使用动态导入进行代码分割和性能优化
  3. 导航守卫: 实现适当的身份验证和授权检查
  4. 数据获取: 根据用户体验需求选择合适的策略
  5. 错误处理: 提供有意义的错误消息和回退路由
  6. SEO 优化: 使用适当的元标签和服务端渲染
  7. 测试: 为路由逻辑和导航流程编写全面的测试

下一步

🚀 Vue Router - 让前端路由变得简单而强大 | 构建现代化单页应用的最佳选择