示例
探索 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 routervue
<!-- 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('欢迎来到仪表板')
})
})最佳实践总结
- 路由组织: 分组相关路由,使用嵌套路由处理层次结构
- 懒加载: 使用动态导入进行代码分割和性能优化
- 导航守卫: 实现适当的身份验证和授权检查
- 数据获取: 根据用户体验需求选择合适的策略
- 错误处理: 提供有意义的错误消息和回退路由
- SEO 优化: 使用适当的元标签和服务端渲染
- 测试: 为路由逻辑和导航流程编写全面的测试
下一步
- 探索 Vue Router API 参考 获取详细文档
- 查看 Vue Router GitHub 仓库 获取更多示例