Vue Router 完全指南:作用与组件详解
Vue Router 是 Vue.js 官方的路由管理器,它让构建单页面应用(SPA)变得简单而强大。
一、Vue Router 的核心作用
1. 单页面应用(SPA)导航
javascript
// 传统多页面应用 vs Vue SPA
传统网站:page1.html → 刷新 → page2.html → 刷新 → page3.html
Vue SPA:index.html → 无刷新切换 → 组件A → 无刷新切换 → 组件B
2. 主要功能
javascript
// main.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
Vue.use(VueRouter)
// 1. 路由定义 - 声明式路由映射
const routes = [
{
path: '/', // URL路径
name: 'Home', // 路由名称
component: Home, // 对应组件
meta: { requiresAuth: true }, // 路由元信息
props: true, // 启用props传参
beforeEnter: (to, from, next) => { // 路由独享守卫
// 权限检查
if (!isAuthenticated()) {
next('/login')
} else {
next()
}
}
},
{
path: '/user/:id', // 动态路由
component: User,
children: [ // 嵌套路由
{ path: 'profile', component: Profile },
{ path: 'posts', component: UserPosts }
]
},
{
path: '/about',
component: () => import('./views/About.vue') // 路由懒加载
}
]
// 2. 创建路由器实例
const router = new VueRouter({
mode: 'history', // 路由模式:history/hash
base: process.env.BASE_URL, // 基路径
routes, // 路由配置
scrollBehavior(to, from, savedPosition) {
// 滚动行为控制
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
},
linkActiveClass: 'active-link', // 激活链接的class
linkExactActiveClass: 'exact-active-link'
})
// 3. 挂载到Vue实例
new Vue({
router, // 注入路由,让整个应用都有路由功能
render: h => h(App)
}).$mount('#app')
二、Vue Router 的核心组件详解
1. <router-link> - 声明式导航
vue
<!-- 基础用法 -->
<template>
<div>
<!-- 1. 基本链接 -->
<router-link to="/home">首页</router-link>
<!-- 2. 使用命名路由 -->
<router-link :to="{ name: 'user', params: { id: 123 }}">
用户资料
</router-link>
<!-- 3. 带查询参数 -->
<router-link :to="{ path: '/search', query: { q: 'vue' } }">
搜索 Vue
</router-link>
<!-- 4. 替换当前历史记录 -->
<router-link to="/about" replace>关于我们</router-link>
<!-- 5. 自定义激活样式 -->
<router-link
to="/contact"
active-class="active-nav"
exact-active-class="exact-active-nav"
>
联系我们
</router-link>
<!-- 6. 渲染其他标签 -->
<router-link to="/help" tag="button" class="help-btn">
帮助中心
</router-link>
<!-- 7. 事件处理 -->
<router-link
to="/dashboard"
@click.native="handleNavClick"
>
控制面板
</router-link>
<!-- 8. 自定义内容 -->
<router-link to="/cart">
<i class="icon-cart"></i>
<span class="badge">{{ cartCount }}</span>
购物车
</router-link>
<!-- 9. 激活时自动添加类名 -->
<nav>
<router-link
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="nav-item"
>
{{ item.title }}
</router-link>
</nav>
</div>
</template>
<script>
export default {
data() {
return {
cartCount: 3,
navItems: [
{ path: '/', title: '首页' },
{ path: '/products', title: '产品' },
{ path: '/services', title: '服务' },
{ path: '/blog', title: '博客' }
]
}
},
methods: {
handleNavClick(event) {
console.log('导航点击:', event)
// 可以在这里添加跟踪代码
this.$analytics.track('navigation_click', {
target: event.target.getAttribute('href')
})
}
}
}
</script>
<style>
/* 激活状态样式 */
.active-nav {
color: #007bff;
font-weight: bold;
border-bottom: 2px solid #007bff;
}
.exact-active-nav {
background-color: #007bff;
color: white;
}
.nav-item {
padding: 10px 15px;
text-decoration: none;
color: #333;
transition: all 0.3s;
}
.nav-item:hover {
background-color: #f8f9fa;
}
.nav-item.router-link-active {
background-color: #e9ecef;
color: #007bff;
}
.nav-item.router-link-exact-active {
background-color: #007bff;
color: white;
}
.help-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #28a745;
color: white;
cursor: pointer;
}
.help-btn:hover {
background: #218838;
}
</style>
2. <router-view> - 路由出口
vue
<!-- App.vue - 应用根组件 -->
<template>
<div id="app">
<!-- 1. 顶部导航栏 -->
<header class="app-header">
<nav class="main-nav">
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-link to="/products">产品</router-link>
<router-link to="/contact">联系</router-link>
</nav>
<!-- 用户信息显示区域 -->
<div class="user-info" v-if="$route.meta.showUserInfo">
<span>欢迎, {{ userName }}</span>
</div>
</header>
<!-- 2. 主内容区域 -->
<main class="app-main">
<!-- 路由出口 - 一级路由 -->
<router-view></router-view>
</main>
<!-- 3. 页脚 -->
<footer class="app-footer" v-if="!$route.meta.hideFooter">
<p>© 2024 我的应用</p>
</footer>
<!-- 4. 全局加载状态 -->
<div v-if="$route.meta.isLoading" class="global-loading">
加载中...
</div>
<!-- 5. 全局错误提示 -->
<div v-if="$route.meta.hasError" class="global-error">
页面加载失败,请重试
</div>
</div>
</template>
<script>
export default {
computed: {
userName() {
return this.$store.state.user?.name || '游客'
}
},
watch: {
// 监听路由变化
'$route'(to, from) {
console.log('路由变化:', from.path, '→', to.path)
// 页面访问统计
this.trackPageView(to)
// 滚动到顶部
if (to.meta.scrollToTop !== false) {
window.scrollTo(0, 0)
}
}
},
methods: {
trackPageView(route) {
// 发送页面访问统计
this.$analytics.pageView({
path: route.path,
name: route.name,
params: route.params,
query: route.query
})
}
}
}
</script>
<style>
#app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-header {
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.main-nav a {
margin: 0 15px;
text-decoration: none;
color: #333;
}
.app-main {
flex: 1;
padding: 20px;
}
.app-footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
border-top: 1px solid #dee2e6;
}
.global-loading,
.global-error {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 10px;
text-align: center;
z-index: 9999;
}
.global-loading {
background: #ffc107;
color: #856404;
}
.global-error {
background: #dc3545;
color: white;
}
</style>
3. 命名视图 - 多视图组件
javascript
// router/index.js - 命名视图配置
const routes = [
{
path: '/dashboard',
components: {
default: DashboardLayout, // 默认视图
header: DashboardHeader, // 命名视图:header
sidebar: DashboardSidebar, // 命名视图:sidebar
footer: DashboardFooter // 命名视图:footer
},
children: [
{
path: 'overview',
components: {
default: OverviewContent,
sidebar: OverviewSidebar
}
},
{
path: 'analytics',
components: {
default: AnalyticsContent,
sidebar: AnalyticsSidebar
}
}
]
}
]
vue
<!-- DashboardLayout.vue -->
<template>
<div class="dashboard-container">
<!-- 命名视图渲染 -->
<header class="dashboard-header">
<router-view name="header"></router-view>
</header>
<div class="dashboard-body">
<!-- 左侧边栏 -->
<aside class="dashboard-sidebar">
<router-view name="sidebar"></router-view>
</aside>
<!-- 主内容区域 -->
<main class="dashboard-main">
<!-- 默认视图 -->
<router-view></router-view>
</main>
</div>
<!-- 页脚 -->
<footer class="dashboard-footer">
<router-view name="footer"></router-view>
</footer>
</div>
</template>
<style>
.dashboard-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.dashboard-body {
display: flex;
flex: 1;
}
.dashboard-sidebar {
width: 250px;
background: #f8f9fa;
border-right: 1px solid #dee2e6;
}
.dashboard-main {
flex: 1;
padding: 20px;
}
.dashboard-header,
.dashboard-footer {
background: #fff;
border-bottom: 1px solid #dee2e6;
padding: 15px 20px;
}
</style>
vue
<!-- DashboardHeader.vue -->
<template>
<div class="dashboard-header">
<div class="header-left">
<h1>{{ currentPageTitle }}</h1>
<nav class="breadcrumb">
<router-link to="/dashboard">仪表板</router-link>
<span v-if="$route.name"> / {{ $route.meta.title }}</span>
</nav>
</div>
<div class="header-right">
<!-- 用户操作 -->
<div class="user-actions">
<button @click="toggleTheme" class="theme-toggle">
{{ isDarkTheme ? '🌙' : '☀️' }}
</button>
<button @click="showNotifications" class="notifications-btn">
🔔 <span class="badge">{{ unreadCount }}</span>
</button>
<div class="user-menu">
<img :src="user.avatar" alt="头像" class="user-avatar">
<span class="user-name">{{ user.name }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
currentPageTitle() {
return this.$route.meta.title || '仪表板'
},
isDarkTheme() {
return this.$store.state.theme === 'dark'
},
user() {
return this.$store.state.user
},
unreadCount() {
return this.$store.getters.unreadNotifications
}
},
methods: {
toggleTheme() {
this.$store.dispatch('toggleTheme')
},
showNotifications() {
this.$router.push('/notifications')
}
}
}
</script>
<style>
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left h1 {
margin: 0;
font-size: 1.5rem;
}
.breadcrumb {
font-size: 0.9rem;
color: #6c757d;
}
.breadcrumb a {
color: #007bff;
text-decoration: none;
}
.user-actions {
display: flex;
align-items: center;
gap: 15px;
}
.theme-toggle,
.notifications-btn {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
position: relative;
}
.badge {
position: absolute;
top: -5px;
right: -5px;
background: #dc3545;
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 0.7rem;
display: flex;
align-items: center;
justify-content: center;
}
.user-menu {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.user-name {
font-weight: 500;
}
</style>
4. 嵌套 <router-view> - 嵌套路由
javascript
// router/index.js - 嵌套路由配置
const routes = [
{
path: '/user/:id',
component: UserLayout,
children: [
// UserProfile 将被渲染在 UserLayout 的 <router-view> 中
{
path: '', // 默认子路由
name: 'user',
component: UserProfile,
meta: { requiresAuth: true }
},
{
path: 'posts',
name: 'userPosts',
component: UserPosts,
props: true // 将路由参数作为 props 传递
},
{
path: 'settings',
component: UserSettings,
children: [ // 多层嵌套
{
path: 'profile',
component: ProfileSettings
},
{
path: 'security',
component: SecuritySettings
}
]
}
]
}
]
vue
<!-- UserLayout.vue - 用户布局组件 -->
<template>
<div class="user-layout">
<!-- 用户信息卡片 -->
<div class="user-info-card">
<img :src="user.avatar" alt="头像" class="user-avatar-large">
<h2>{{ user.name }}</h2>
<p class="user-bio">{{ user.bio }}</p>
<!-- 用户导航 -->
<nav class="user-nav">
<router-link
:to="{ name: 'user', params: { id: $route.params.id } }"
exact
>
概览
</router-link>
<router-link :to="`/user/${$route.params.id}/posts`">
文章 ({{ user.postCount }})
</router-link>
<router-link :to="`/user/${$route.params.id}/photos`">
相册
</router-link>
<router-link :to="`/user/${$route.params.id}/friends`">
好友 ({{ user.friendCount }})
</router-link>
<router-link :to="`/user/${$route.params.id}/settings`">
设置
</router-link>
</nav>
</div>
<!-- 嵌套路由出口 -->
<div class="user-content">
<router-view></router-view>
</div>
<!-- 三级嵌套路由出口(在用户设置中) -->
<div v-if="$route.path.includes('/settings')" class="settings-layout">
<aside class="settings-sidebar">
<router-view name="settingsNav"></router-view>
</aside>
<main class="settings-main">
<router-view name="settingsContent"></router-view>
</main>
</div>
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: '加载中...',
avatar: '',
bio: '',
postCount: 0,
friendCount: 0
}
}
},
watch: {
'$route.params.id': {
immediate: true,
handler(userId) {
this.loadUserData(userId)
}
}
},
methods: {
async loadUserData(userId) {
try {
const response = await this.$api.getUser(userId)
this.user = response.data
} catch (error) {
console.error('加载用户数据失败:', error)
this.$router.push('/error')
}
}
}
}
</script>
<style>
.user-layout {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.user-info-card {
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 30px;
text-align: center;
}
.user-avatar-large {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 20px;
}
.user-bio {
color: #666;
margin: 15px 0;
font-size: 1rem;
}
.user-nav {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
border-top: 1px solid #eee;
padding-top: 20px;
}
.user-nav a {
padding: 8px 16px;
text-decoration: none;
color: #333;
border-radius: 4px;
transition: all 0.3s;
}
.user-nav a:hover {
background: #f8f9fa;
}
.user-nav a.router-link-active {
background: #007bff;
color: white;
}
.user-content {
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.settings-layout {
display: flex;
gap: 30px;
margin-top: 30px;
}
.settings-sidebar {
width: 250px;
flex-shrink: 0;
}
.settings-main {
flex: 1;
}
</style>
三、路由组件的高级特性
1. 路由过渡效果
vue
<!-- App.vue - 添加路由过渡 -->
<template>
<div id="app">
<!-- 导航栏 -->
<nav class="main-nav">...</nav>
<!-- 路由过渡 -->
<transition :name="transitionName" mode="out-in">
<router-view class="router-view" :key="$route.fullPath" />
</transition>
<!-- 嵌套路由过渡 -->
<transition-group name="fade" tag="div" class="nested-routes">
<router-view
v-for="view in nestedViews"
:key="view.key"
:name="view.name"
/>
</transition-group>
</div>
</template>
<script>
export default {
data() {
return {
transitionName: 'fade',
previousDepth: 0
}
},
computed: {
nestedViews() {
// 动态生成嵌套视图配置
return this.$route.matched.map((route, index) => ({
name: route.components.default.name,
key: route.path + index
}))
}
},
watch: {
'$route'(to, from) {
// 根据路由深度决定过渡动画
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
}
}
}
</script>
<style>
/* 淡入淡出效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
/* 滑动效果 */
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: all 0.3s ease;
}
.slide-left-enter {
opacity: 0;
transform: translateX(30px);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.slide-right-enter {
opacity: 0;
transform: translateX(-30px);
}
.slide-right-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 路由视图样式 */
.router-view {
position: relative;
min-height: calc(100vh - 120px);
}
.nested-routes {
position: relative;
}
</style>
2. 路由懒加载与代码分割
javascript
// router/index.js - 动态导入实现懒加载
const routes = [
{
path: '/',
name: 'Home',
// 1. 使用动态导入
component: () => import('@/views/Home.vue'),
// 2. 分组打包
meta: {
chunkName: 'main' // 指定webpack chunk名
}
},
{
path: '/dashboard',
// 3. 懒加载布局和内容
component: () => import('@/layouts/DashboardLayout.vue'),
children: [
{
path: '',
component: () => import('@/views/dashboard/Overview.vue'),
meta: {
requiresAuth: true,
preload: true // 标记为预加载
}
},
{
path: 'analytics',
// 4. 魔法注释指定webpack chunk
component: () => import(/* webpackChunkName: "analytics" */ '@/views/dashboard/Analytics.vue')
},
{
path: 'reports',
// 5. 条件导入
component: () => {
if (userIsAdmin()) {
return import('@/views/dashboard/AdminReports.vue')
} else {
return import('@/views/dashboard/UserReports.vue')
}
}
}
]
},
{
path: '/admin',
// 6. 预加载(在空闲时加载)
component: () => import(/* webpackPrefetch: true */ '@/views/Admin.vue'),
meta: {
requiresAdmin: true
}
}
]
// 路由守卫中动态加载
router.beforeEach(async (to, from, next) => {
// 检查是否需要验证
if (to.matched.some(record => record.meta.requiresAuth)) {
// 动态加载用户模块
const { checkAuth } = await import('@/utils/auth')
if (!checkAuth()) {
next('/login')
return
}
}
// 预加载下一路由
if (to.meta.preload) {
const matched = to.matched[to.matched.length - 1]
if (matched && matched.components) {
matched.components.default().catch(() => {
// 加载失败处理
})
}
}
next()
})
3. 滚动行为控制
javascript
// router/index.js - 自定义滚动行为
const router = new VueRouter({
routes,
scrollBehavior(to, from, savedPosition) {
// 1. 返回保存的位置(浏览器前进/后退)
if (savedPosition) {
return savedPosition
}
// 2. 滚动到指定锚点
if (to.hash) {
return {
selector: to.hash,
behavior: 'smooth',
offset: { x: 0, y: 100 } // 偏移量
}
}
// 3. 特定路由滚动到顶部
if (to.meta.scrollToTop !== false) {
return { x: 0, y: 0 }
}
// 4. 保持当前位置
return false
}
})
// 在组件中手动控制滚动
export default {
methods: {
scrollToElement(selector) {
this.$nextTick(() => {
const element = document.querySelector(selector)
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
})
},
// 保存滚动位置
saveScrollPosition() {
this.scrollPosition = {
x: window.pageXOffset,
y: window.pageYOffset
}
},
// 恢复滚动位置
restoreScrollPosition() {
if (this.scrollPosition) {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y)
}
}
},
beforeRouteLeave(to, from, next) {
// 离开路由前保存位置
this.saveScrollPosition()
next()
},
activated() {
// 组件激活时恢复位置
this.restoreScrollPosition()
}
}
四、实际项目案例
案例1:电商网站路由设计
javascript
// router/index.js - 电商路由配置
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
title: '首页 - 我的商城',
keepAlive: true,
showFooter: true
}
},
{
path: '/products',
component: () => import('@/layouts/ProductLayout.vue'),
meta: { showCategorySidebar: true },
children: [
{
path: '',
name: 'ProductList',
component: () => import('@/views/products/ProductList.vue'),
props: route => ({
category: route.query.category,
sort: route.query.sort,
page: parseInt(route.query.page) || 1
})
},
{
path: ':id',
name: 'ProductDetail',
component: () => import('@/views/products/ProductDetail.vue'),
props: true,
meta: {
title: '商品详情',
showBreadcrumb: true
}
},
{
path: ':id/reviews',
name: 'ProductReviews',
component: () => import('@/views/products/ProductReviews.vue'),
meta: { requiresPurchase: true }
}
]
},
{
path: '/cart',
name: 'Cart',
component: () => import('@/views/Cart.vue'),
meta: {
requiresAuth: true,
title: '购物车'
},
beforeEnter: (to, from, next) => {
// 检查购物车是否为空
const cartStore = useCartStore()
if (cartStore.items.length === 0) {
next({ name: 'EmptyCart' })
} else {
next()
}
}
},
{
path: '/checkout',
component: () => import('@/layouts/CheckoutLayout.vue'),
meta: { requiresAuth: true, hideFooter: true },
children: [
{
path: 'shipping',
name: 'CheckoutShipping',
component: () => import('@/views/checkout/Shipping.vue')
},
{
path: 'payment',
name: 'CheckoutPayment',
component: () => import('@/views/checkout/Payment.vue'),
beforeEnter: (to, from, next) => {
// 确保已经填写了配送信息
if (!from.name.includes('Checkout')) {
next({ name: 'CheckoutShipping' })
} else {
next()
}
}
},
{
path: 'confirm',
name: 'CheckoutConfirm',
component: () => import('@/views/checkout/Confirm.vue')
}
]
},
{
path: '/user',
component: () => import('@/layouts/UserLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: 'orders',
name: 'UserOrders',
component: () => import('@/views/user/Orders.vue'),
meta: { showOrderFilter: true }
},
{
path: 'orders/:orderId',
name: 'OrderDetail',
component: () => import('@/views/user/OrderDetail.vue'),
props: true
},
{
path: 'wishlist',
name: 'Wishlist',
component: () => import('@/views/user/Wishlist.vue'),
meta: { keepAlive: true }
},
{
path: 'settings',
redirect: { name: 'ProfileSettings' }
}
]
},
{
path: '/search',
name: 'Search',
component: () => import('@/views/Search.vue'),
props: route => ({
query: route.query.q,
filters: JSON.parse(route.query.filters || '{}')
})
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
meta: { title: '页面未找到' }
},
{
path: '*',
redirect: '/404'
}
]
// 路由全局守卫
router.beforeEach(async (to, from, next) => {
// 设置页面标题
document.title = to.meta.title || '我的商城'
// 用户认证检查
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next({
name: 'Login',
query: { redirect: to.fullPath }
})
return
}
// 权限检查
if (to.meta.requiresAdmin && !authStore.isAdmin) {
next({ name: 'Forbidden' })
return
}
// 添加页面访问记录
trackPageView(to)
next()
})
// 路由独享守卫示例
const checkoutRoutes = [
{
path: '/checkout/shipping',
beforeEnter: (to, from, next) => {
const cartStore = useCartStore()
if (cartStore.items.length === 0) {
next({ name: 'Cart' })
} else {
next()
}
}
}
]
// 滚动行为
router.scrollBehavior = (to, from, savedPosition) => {
if (savedPosition) {
return savedPosition
}
if (to.hash) {
return {
selector: to.hash,
behavior: 'smooth'
}
}
// 商品列表保持滚动位置
if (from.name === 'ProductList' && to.name === 'ProductDetail') {
return false
}
return { x: 0, y: 0 }
}
案例2:后台管理系统路由
javascript
// router/modules/admin.js - 后台管理路由模块
const adminRoutes = [
{
path: '/admin',
redirect: '/admin/dashboard',
meta: {
requiresAuth: true,
requiresAdmin: true,
layout: 'AdminLayout'
}
},
{
path: '/admin/dashboard',
name: 'AdminDashboard',
component: () => import('@/views/admin/Dashboard.vue'),
meta: {
title: '控制面板',
icon: 'dashboard',
breadcrumb: ['首页', '控制面板']
}
},
{
path: '/admin/users',
component: () => import('@/layouts/admin/UserLayout.vue'),
meta: {
title: '用户管理',
icon: 'user',
permission: 'user:view'
},
children: [
{
path: '',
name: 'UserList',
component: () => import('@/views/admin/users/List.vue'),
meta: {
title: '用户列表',
keepAlive: true,
cacheKey: 'userList'
}
},
{
path: 'create',
name: 'UserCreate',
component: () => import('@/views/admin/users/Create.vue'),
meta: {
title: '创建用户',
permission: 'user:create'
}
},
{
path: 'edit/:id',
name: 'UserEdit',
component: () => import('@/views/admin/users/Edit.vue'),
props: true,
meta: {
title: '编辑用户',
permission: 'user:edit'
}
},
{
path: 'roles',
name: 'RoleManagement',
component: () => import('@/views/admin/users/Roles.vue'),
meta: {
title: '角色管理',
permission: 'role:view'
}
}
]
},
{
path: '/admin/content',
meta: { title: '内容管理', icon: 'content' },
children: [
{
path: 'articles',
name: 'ArticleList',
component: () => import('@/views/admin/content/Articles.vue'),
meta: { title: '文章管理' }
},
{
path: 'categories',
name: 'CategoryList',
component: () => import('@/views/admin/content/Categories.vue')
}
]
},
{
path: '/admin/system',
meta: { title: '系统设置', icon: 'setting' },
children: [
{
path: 'settings',
name: 'SystemSettings',
component: () => import('@/views/admin/system/Settings.vue'),
meta: { requiresSuperAdmin: true }
},
{
path: 'logs',
name: 'SystemLogs',
component: () => import('@/views/admin/system/Logs.vue')
}
]
}
]
// 动态路由加载(基于权限)
export function generateRoutes(userPermissions) {
const routes = []
adminRoutes.forEach(route => {
if (hasPermission(route.meta?.permission, userPermissions)) {
routes.push(route)
}
})
return routes
}
// 路由守卫 - 权限检查
router.beforeEach((to, from, next) => {
// 获取用户权限
const permissions = store.getters.userPermissions
// 检查路由权限
if (to.meta.permission && !hasPermission(to.meta.permission, permissions)) {
next({ name: 'Forbidden' })
return
}
// 检查超级管理员权限
if (to.meta.requiresSuperAdmin && !store.getters.isSuperAdmin) {
next({ name: 'Forbidden' })
return
}
next()
})
// 面包屑导航
router.afterEach((to) => {
// 生成面包屑
const breadcrumb = []
to.matched.forEach(route => {
if (route.meta.breadcrumb) {
breadcrumb.push(...route.meta.breadcrumb)
} else if (route.meta.title) {
breadcrumb.push(route.meta.title)
}
})
// 存储到状态管理中
store.commit('SET_BREADCRUMB', breadcrumb)
})
五、最佳实践总结
1. 路由组织建议
javascript
// 推荐的项目结构
src/
├── router/
│ ├── index.js # 主路由文件
│ ├── modules/ # 路由模块
│ │ ├── auth.js # 认证相关路由
│ │ ├── admin.js # 管理后台路由
│ │ ├── shop.js # 商城路由
│ │ └── blog.js # 博客路由
│ └── guards/ # 路由守卫
│ ├── auth.js # 认证守卫
│ ├── permission.js # 权限守卫
│ └── progress.js # 进度条守卫
├── views/ # 路由组件
│ ├── Home.vue
│ ├── About.vue
│ ├── user/ # 用户相关视图
│ ├── admin/ # 管理视图
│ └── ...
└── layouts/ # 布局组件
├── DefaultLayout.vue
├── AdminLayout.vue
└── ...
2. 性能优化技巧
javascript
// 1. 路由懒加载
component: () => import('@/views/HeavyComponent.vue')
// 2. 预加载关键路由
router.beforeEach((to, from, next) => {
if (to.meta.preload) {
import('@/views/CriticalComponent.vue')
}
next()
})
// 3. 路由组件缓存
<keep-alive :include="cachedRoutes">
<router-view :key="$route.fullPath" />
</keep-alive>
// 4. 滚动位置恢复
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
// 特定路由保持位置
if (from.name === 'ProductList' && to.name === 'ProductDetail') {
return false
}
return { x: 0, y: 0 }
}
// 5. 路由数据预取
{
path: '/product/:id',
component: ProductDetail,
async beforeRouteEnter(to, from, next) {
// 预取数据
const product = await fetchProduct(to.params.id)
next(vm => vm.setProduct(product))
}
}
3. 错误处理与降级
javascript
// 全局错误处理
router.onError((error) => {
console.error('路由错误:', error)
// 组件加载失败
if (/Loading chunk (\d)+ failed/.test(error.message)) {
// 重新加载页面
window.location.reload()
}
})
// 404处理
router.beforeEach((to, from, next) => {
if (!to.matched.length) {
next('/404')
} else {
next()
}
})
// 网络异常处理
router.beforeEach((to, from, next) => {
if (!navigator.onLine && to.meta.requiresOnline) {
next('/offline')
} else {
next()
}
})
// 降级方案
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
.catch(() => import('@/views/DashboardFallback.vue'))
}
]
4. TypeScript支持
typescript
// router/types.ts - 类型定义
import { RouteConfig } from 'vue-router'
declare module 'vue-router/types/router' {
interface RouteMeta {
title?: string
requiresAuth?: boolean
permission?: string
keepAlive?: boolean
icon?: string
breadcrumb?: string[]
}
}
// 路由配置
const routes: RouteConfig[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
title: '首页',
requiresAuth: true
}
}
]
// 组件内使用
import { Vue, Component } from 'vue-property-decorator'
import { Route } from 'vue-router'
@Component
export default class UserProfile extends Vue {
// 路由参数类型
@Prop({ type: String, required: true })
readonly id!: string
// 路由对象
get route(): Route {
return this.$route
}
// 编程式导航
goToSettings() {
this.$router.push({
name: 'UserSettings',
params: { userId: this.id }
})
}
}
六、Vue Router 4(Vue 3)新特性
javascript
// Vue Router 4 示例
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('@/views/Home.vue'),
// 新的路由元字段
meta: {
transition: 'fade'
}
}
]
})
// 组合式API使用
import { useRoute, useRouter } from 'vue-router'
import { computed } from 'vue'
export default {
setup() {
const route = useRoute()
const router = useRouter()
// 响应式路由参数
const userId = computed(() => route.params.id)
// 编程式导航
const goBack = () => router.back()
// 监听路由变化
watch(() => route.path, (newPath) => {
console.log('路由变化:', newPath)
})
return { userId, goBack }
}
}
总结 :Vue Router 提供了完整的客户端路由解决方案,通过 <router-link> 和 <router-view> 等组件,结合路由配置、导航守卫、懒加载等特性,可以构建出功能丰富、性能优秀的单页面应用。合理使用这些特性,可以大幅提升用户体验和开发效率。