Vue Router 完全指南:作用与组件详解

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>&copy; 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> 等组件,结合路由配置、导航守卫、懒加载等特性,可以构建出功能丰富、性能优秀的单页面应用。合理使用这些特性,可以大幅提升用户体验和开发效率。

相关推荐
北辰alk6 小时前
Vue 中使用 this 的完整指南与注意事项
vue.js
xkxnq6 小时前
第二阶段:Vue 组件化开发(第 16天)
前端·javascript·vue.js
北辰alk6 小时前
Vue 插槽(Slot)完全指南:组件内容分发的艺术
vue.js
北辰alk7 小时前
Vue 组件中访问根实例的完整指南
vue.js
北辰alk7 小时前
Vue Router 中获取路由参数的全面指南
vue.js
北辰alk7 小时前
Vue 的 v-cloak 和 v-pre 指令详解
vue.js
期待のcode7 小时前
前后端分离项目 Springboot+vue 在云服务器上的部署
服务器·vue.js·spring boot
xkxnq7 小时前
第一阶段:Vue 基础入门(第 15天)
前端·javascript·vue.js
北辰alk7 小时前
Vue 过滤器:优雅处理数据的艺术
vue.js