【Vue】路由管理(Vue Router)

在这里插入图片描述

个人主页:Guiat
归属专栏:Vue

在这里插入图片描述

正文

1. Vue Router 简介

Vue Router 是 Vue.js 官方的路由管理器,它与 Vue.js 核心深度集成,使构建单页面应用变得轻而易举。它提供了声明式的导航控制和组件系统,让开发者能够通过配置路由映射、嵌套路由等方式构建复杂的应用。

1.1 单页应用与路由

单页应用(SPA)是一种网页应用程序或网站的模型,它通过动态重写当前页面来与用户交互,而非传统的从服务器加载整个新页面。这种方法避免了页面之间切换打断用户体验。

// 传统多页应用
// 每次导航都会向服务器请求新页面
location.href = '/about.html'

// 单页应用
// 只改变 URL,不重新加载页面
history.pushState(null, '', '/about')

1.2 Vue Router 的核心功能

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue.js 过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5 历史模式或 hash 模式
  • 自定义的滚动行为

2. Vue Router 基础配置

2.1 安装与基本设置

# Vue 2
npm install vue-router@3

# Vue 3
npm install vue-router@4

基本配置(Vue 3):

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

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

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

export default router

在 Vue 应用中使用:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

2.2 路由模式

Vue Router 提供两种路由模式:

  1. Hash 模式:使用 URL 的 hash (#) 来模拟完整的 URL
  2. History 模式:利用 HTML5 History API 实现无 hash 的 URL
// Hash 模式 (默认)
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// History 模式
const router = createRouter({
  history: createWebHistory(),
  routes
})

2.3 路由组件与视图

在 Vue 组件中使用路由视图:

<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    
    <!-- 路由匹配的组件将渲染在这里 -->
    <router-view />
  </div>
</template>

3. 路由导航

3.1 声明式导航

使用 <router-link> 组件进行声明式导航:

<!-- 基本用法 -->
<router-link to="/">Home</router-link>

<!-- 命名路由 -->
<router-link :to="{ name: 'About' }">About</router-link>

<!-- 带参数的路由 -->
<router-link :to="{ name: 'User', params: { id: 123 }}">User</router-link>

<!-- 带查询参数 -->
<router-link :to="{ path: '/search', query: { q: 'vue' }}">Search</router-link>

<!-- 自定义激活类 -->
<router-link to="/about" active-class="active">About</router-link>

<!-- 精确匹配 (Vue 3) -->
<router-link to="/" :exact="true">Home</router-link>

3.2 编程式导航

通过 JavaScript 代码控制路由:

// 基本导航
this.$router.push('/')
this.$router.push('/about')

// 对象形式
this.$router.push({ path: '/about' })

// 命名路由
this.$router.push({ name: 'User', params: { id: 123 }})

// 带查询参数
this.$router.push({ path: '/search', query: { q: 'vue' }})

// 替换当前历史记录
this.$router.replace('/about')

// 前进/后退
this.$router.go(1)   // 前进一步
this.$router.go(-1)  // 后退一步
this.$router.back()  // 后退一步
this.$router.forward() // 前进一步

在 Vue 3 Composition API 中:

import { useRouter } from 'vue-router'

export default {
  setup() {
    const router = useRouter()
    
    function navigateToAbout() {
      router.push('/about')
    }
    
    return { navigateToAbout }
  }
}

3.3 路由参数获取

通过 $route 对象访问路由参数:

// 路由配置
const routes = [
  {
    path: '/user/:id',
    name: 'User',
    component: User
  }
]

// 在组件中获取参数 (Vue 2)
export default {
  created() {
    // 路径参数
    console.log(this.$route.params.id)
    
    // 查询参数
    console.log(this.$route.query.search)
  }
}

// 在组件中获取参数 (Vue 3 Composition API)
import { useRoute } from 'vue-router'

export default {
  setup() {
    const route = useRoute()
    console.log(route.params.id)
    console.log(route.query.search)
  }
}

4. 高级路由配置

4.1 嵌套路由

路由可以嵌套,实现复杂的应用布局:

const routes = [
  {
    path: '/user',
    component: User,
    children: [
      // 当 /user 匹配成功
      // UserHome 会被渲染在 User 的 <router-view> 中
      { path: '', component: UserHome },
      
      // 当 /user/profile 匹配成功
      // UserProfile 会被渲染在 User 的 <router-view> 中
      { path: 'profile', component: UserProfile },
      
      // 当 /user/posts 匹配成功
      // UserPosts 会被渲染在 User 的 <router-view> 中
      { path: 'posts', component: UserPosts }
    ]
  }
]

对应的组件结构:

<!-- User.vue -->
<template>
  <div>
    <h2>User Section</h2>
    <nav>
      <router-link to="/user">Home</router-link>
      <router-link to="/user/profile">Profile</router-link>
      <router-link to="/user/posts">Posts</router-link>
    </nav>
    <router-view></router-view>
  </div>
</template>

4.2 命名视图

同一个路由可以显示多个视图:

const routes = [
  {
    path: '/',
    components: {
      default: Home,
      sidebar: Sidebar,
      footer: Footer
    }
  }
]

对应的模板:

<router-view></router-view>
<router-view name="sidebar"></router-view>
<router-view name="footer"></router-view>

4.3 路由懒加载

为了提高应用性能,可以使用动态导入实现路由懒加载:

// 不使用懒加载
import UserDetails from './views/UserDetails.vue'

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

// 使用懒加载
const routes = [
  { 
    path: '/user/:id', 
    component: () => import('./views/UserDetails.vue') 
  }
]

// 分组懒加载
const routes = [
  {
    path: '/user',
    component: () => import(/* webpackChunkName: "user" */ './views/User.vue'),
    children: [
      {
        path: 'profile',
        component: () => import(/* webpackChunkName: "user" */ './views/UserProfile.vue')
      },
      {
        path: 'posts',
        component: () => import(/* webpackChunkName: "user" */ './views/UserPosts.vue')
      }
    ]
  }
]

5. 路由守卫

5.1 全局守卫

全局前置守卫:

router.beforeEach((to, from, next) => {
  // to: 即将进入的目标路由对象
  // from: 当前导航正要离开的路由对象
  // next: 函数,必须调用该方法来 resolve 这个钩子
  
  // 检查用户是否已登录
  if (to.meta.requiresAuth && !isAuthenticated) {
    // 未登录,重定向到登录页
    next({ name: 'Login' })
  } else {
    // 继续导航
    next()
  }
})

全局解析守卫:

router.beforeResolve((to, from, next) => {
  // 在所有组件内守卫和异步路由组件被解析之后调用
  next()
})

全局后置钩子:

router.afterEach((to, from) => {
  // 不会影响导航本身,常用于分析、更改页面标题等
  document.title = to.meta.title || 'My App'
})

5.2 路由独享守卫

直接在路由配置上定义:

const routes = [
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from, next) => {
      // 检查用户权限
      if (hasAdminRights()) {
        next()
      } else {
        next('/403')
      }
    }
  }
]

5.3 组件内守卫

在组件内定义路由导航守卫:

export default {
  // 在渲染该组件的对应路由被验证前调用
  beforeRouteEnter(to, from, next) {
    // 不能获取组件实例 `this`
    // 因为当守卫执行时,组件实例还没被创建
    next(vm => {
      // 通过 `vm` 访问组件实例
    })
  },
  
  // 在当前路由改变,但是该组件被复用时调用
  beforeRouteUpdate(to, from, next) {
    // 可以访问组件实例 `this`
    this.name = to.params.name
    next()
  },
  
  // 导航离开该组件的对应路由时调用
  beforeRouteLeave(to, from, next) {
    // 可以访问组件实例 `this`
    const answer = window.confirm('Do you really want to leave?')
    if (answer) {
      next()
    } else {
      next(false)
    }
  }
}

在 Vue 3 Composition API 中:

import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

export default {
  setup() {
    // 与 beforeRouteLeave 相同
    onBeforeRouteLeave((to, from) => {
      const answer = window.confirm('Do you really want to leave?')
      if (!answer) return false
    })

    // 与 beforeRouteUpdate 相同
    onBeforeRouteUpdate((to, from) => {
      // 处理路由变化...
    })
  }
}

6. 路由元信息与数据获取

6.1 路由元信息

可以在路由配置中附加自定义数据:

const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: { 
      requiresAuth: true,
      title: 'Admin Panel',
      permissions: ['admin', 'editor']
    }
  }
]

使用元信息:

// 全局守卫中使用
router.beforeEach((to, from, next) => {
  document.title = to.meta.title || 'Default Title'
  
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

// 组件中使用
export default {
  created() {
    console.log(this.$route.meta.permissions)
  }
}

6.2 路由数据获取

在导航完成前获取数据:

export default {
  data() {
    return {
      post: null,
      loading: false,
      error: null
    }
  },
  beforeRouteEnter(to, from, next) {
    next(vm => {
      vm.fetchData()
    })
  },
  beforeRouteUpdate(to, from, next) {
    this.post = null
    this.fetchData()
    next()
  },
  methods: {
    fetchData() {
      this.loading = true
      this.error = null
      
      // 假设 getPost 返回一个 Promise
      getPost(this.$route.params.id)
        .then(post => {
          this.post = post
          this.loading = false
        })
        .catch(err => {
          this.error = err.toString()
          this.loading = false
        })
    }
  }
}

在导航完成后获取数据:

export default {
  data() {
    return {
      post: null,
      loading: false,
      error: null
    }
  },
  created() {
    // 组件创建完后获取数据
    this.fetchData()
  },
  watch: {
    // 路由变化时重新获取数据
    '$route': 'fetchData'
  },
  methods: {
    fetchData() {
      this.loading = true
      this.error = null
      
      getPost(this.$route.params.id)
        .then(post => {
          this.post = post
          this.loading = false
        })
        .catch(err => {
          this.error = err.toString()
          this.loading = false
        })
    }
  }
}

在 Vue 3 Composition API 中:

import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'

export default {
  setup() {
    const route = useRoute()
    const post = ref(null)
    const loading = ref(false)
    const error = ref(null)
    
    const fetchData = async () => {
      loading.value = true
      error.value = null
      
      try {
        post.value = await getPost(route.params.id)
      } catch (err) {
        error.value = err.toString()
      } finally {
        loading.value = false
      }
    }
    
    // 首次加载
    fetchData()
    
    // 路由参数变化时重新获取
    watch(() => route.params.id, fetchData)
    
    return { post, loading, error }
  }
}

7. 动态路由与进阶技巧

7.1 动态添加和删除路由

Vue Router 允许动态修改路由配置:

// 添加路由
router.addRoute({
  path: '/dynamic',
  name: 'Dynamic',
  component: () => import('./views/Dynamic.vue')
})

// 添加嵌套路由
router.addRoute('User', {
  path: 'settings',
  component: UserSettings
})

// 删除路由
// 方法 1:添加一个名字相同的路由
router.addRoute({ path: '/about', name: 'About', component: About })

// 方法 2:通过返回的删除函数
const removeRoute = router.addRoute(route)
removeRoute() // 删除路由

// 方法 3:通过名称删除
router.removeRoute('About')

// 检查路由是否存在
router.hasRoute('About')

// 获取所有路由记录
router.getRoutes()

7.2 路由过渡效果

结合 Vue 的过渡系统实现路由切换动画:

<template>
  <router-view v-slot="{ Component }">
    <transition name="fade" mode="out-in">
      <component :is="Component" />
    </transition>
  </router-view>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

基于路由的动态过渡:

<template>
  <router-view v-slot="{ Component, route }">
    <transition :name="route.meta.transition || 'fade'">
      <component :is="Component" />
    </transition>
  </router-view>
</template>

<script>
export default {
  // 路由配置
  // {
  //   path: '/slide-page',
  //   component: SlidePage,
  //   meta: { transition: 'slide' }
  // }
}
</script>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s;
}
.slide-enter-from,
.slide-leave-to {
  transform: translateX(100%);
}
</style>

7.3 滚动行为

控制页面切换时的滚动位置:

const router = createRouter({
  history: createWebHistory(),
  routes: [...],
  scrollBehavior(to, from, savedPosition) {
    // 返回 savedPosition,在按下 后退/前进 按钮时有效
    if (savedPosition) {
      return savedPosition
    }
    
    // 滚动到锚点
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth',
      }
    }
    
    // 滚动到顶部
    return { top: 0 }
  }
})

8. 实际应用与最佳实践

8.1 路由组织与模块化

大型应用中的路由组织:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import homeRoutes from './modules/home'
import userRoutes from './modules/user'
import shopRoutes from './modules/shop'

const routes = [
  ...homeRoutes,
  ...userRoutes,
  ...shopRoutes,
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('../views/NotFound.vue') }
]

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

export default router

// router/modules/user.js
export default [
  {
    path: '/user',
    component: () => import('@/layouts/UserLayout.vue'),
    children: [
      {
        path: '',
        name: 'UserHome',
        component: () => import('@/views/user/Home.vue')
      },
      {
        path: 'profile',
        name: 'UserProfile',
        component: () => import('@/views/user/Profile.vue')
      }
    ]
  }
]

8.2 权限控制与认证

基于角色的路由访问控制:

// 路由配置
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: { requiresAuth: true, roles: ['admin'] }
  },
  {
    path: '/editor',
    component: Editor,
    meta: { requiresAuth: true, roles: ['editor', 'admin'] }
  }
]

// 全局前置守卫
router.beforeEach((to, from, next) => {
  const { requiresAuth, roles } = to.meta
  const currentUser = getCurrentUser()
  
  if (!requiresAuth) {
    return next()
  }
  
  if (!currentUser) {
    return next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  }
  
  if (roles && !roles.includes(currentUser.role)) {
    return next('/403')
  }
  
  next()
})

8.3 性能优化策略

  1. 路由懒加载:减小初始加载体积
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  }
]
  1. 预加载关键路由:提高用户体验
// 在应用空闲时预加载
const PreloadedView = () => ({
  component: import('./views/PreloadedView.vue'),
  loading: LoadingComponent,
  delay: 200
})
  1. 保持路由组件状态:使用 <keep-alive>
<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component" />
  </keep-alive>
</router-view>
  1. 路由元信息缓存控制
const routes = [
  {
    path: '/user/:id',
    component: UserDetails,
    meta: { keepAlive: true }
  }
]

// 在模板中
<router-view v-slot="{ Component }">
  <keep-alive>
    <component 
      v-if="$route.meta.keepAlive" 
      :is="Component" 
    />
  </keep-alive>
  <component 
    v-if="!$route.meta.keepAlive" 
    :is="Component" 
  />
</router-view>

9. Vue Router 与状态管理的结合

9.1 Vue Router 与 Vuex/Pinia 协作

// store/index.js (Vuex)
export default createStore({
  state: {
    user: null,
    isAuthenticated: false
  },
  mutations: {
    setUser(state, user) {
      state.user = user
      state.isAuthenticated = !!user
    }
  },
  actions: {
    async login({ commit }, credentials) {
      const user = await authService.login(credentials)
      commit('setUser', user)
      return user
    },
    async logout({ commit }) {
      await authService.logout()
      commit('setUser', null)
    }
  }
})

// 路由守卫中使用
router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  const { isAuthenticated } = store.state
  
  if (requiresAuth && !isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

9.2 路由参数同步到状态

// store/index.js
export default createStore({
  state: {
    searchQuery: '',
    currentProductId: null
  },
  mutations: {
    setSearchQuery(state, query) {
      state.searchQuery = query
    },
    setCurrentProductId(state, id) {
      state.currentProductId = id
    }
  }
})

// 组件中
export default {
  created() {
    // 从路由同步到状态
    this.$store.commit('setSearchQuery', this.$route.query.q || '')
    this.$store.commit('setCurrentProductId', this.$route.params.id)
  },
  watch: {
    '$route'(to) {
      this.$store.commit('setSearchQuery', to.query.q || '')
      this.$store.commit('setCurrentProductId', to.params.id)
    }
  }
}

9.3 基于状态的导航守卫

router.beforeEach(async (to, from, next) => {
  // 检查是否需要获取用户信息
  if (
    to.matched.some(record => record.meta.requiresAuth) && 
    store.state.user === null
  ) {
    try {
      // 尝试获取用户信息
      await store.dispatch('fetchCurrentUser')
      next()
    } catch (error) {
      next('/login')
    }
  } else {
    next()
  }
})

10. 测试与调试

10.1 路由单元测试

使用 Vue Test Utils 测试路由组件:

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

// 创建路由实例
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
]

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

test('routing', async () => {
  // 挂载应用
  const wrapper = mount(App, {
    global: {
      plugins: [router]
    }
  })
  
  // 等待路由准备就绪
  await router.isReady()
  
  // 验证初始路由
  expect(wrapper.html()).toContain('Welcome to Home')
  
  // 导航到 About 页面
  await router.push('/about')
  
  // 等待路由变化
  await wrapper.vm.$nextTick()
  
  // 验证新路由内容
  expect(wrapper.html()).toContain('About Page')
})

10.2 路由调试技巧

  1. 使用 Vue Devtools 检查路由状态

  2. 路由日志记录:

router.beforeEach((to, from, next) => {
  console.log(`Navigating from ${from.fullPath} to ${to.fullPath}`)
  next()
})
  1. 路由错误处理:
router.onError((error) => {
  console.error('Router error:', error)
  // 可以上报到错误监控系统
})

10.3 常见问题与解决方案

  1. 页面刷新 404 问题
    在使用 history 模式时,需要服务器配置将所有请求重定向到 index.html
# Nginx 配置
location / {
  try_files $uri $uri/ /index.html;
}
  1. 路由参数变化但组件不更新
    使用 watch 监听路由变化或使用 key 强制更新
<router-view :key="$route.fullPath"></router-view>
  1. 路由守卫中的异步操作
    确保在异步操作完成后调用 next()
router.beforeEach(async (to, from, next) => {
  try {
    await someAsyncOperation()
    next()
  } catch (error) {
    next(false)
  }
})
  1. 导航重复错误
    处理导航到当前位置的情况
// Vue Router 4.x
router.push('/current-path').catch(err => {
  if (err.name !== 'NavigationDuplicated') {
    throw err
  }
})

// 或全局处理
const originalPush = router.push
router.push = function push(location) {
  return originalPush.call(this, location).catch(err => {
    if (err.name !== 'NavigationDuplicated') throw err
  })
}

结语
感谢您的阅读!期待您的一键三连!欢迎指正!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Guiat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值