Skip to content

Vue Router

Vue Router 是 Vue.js 的官方路由管理器。它和 Vue.js 深度集成,让用 Vue.js 构建单页应用变得轻而易举。功能包括:

  • 嵌套路由映射
  • 动态路由选择
  • 模块化、基于组件的路由配置
  • 路由参数、查询、通配符
  • 展示由 Vue.js 的过渡系统提供的过渡效果
  • 细致的导航控制
  • 自动激活 CSS 类的链接
  • HTML5 history 模式或 hash 模式
  • 可定制的滚动行为
  • URL 的正确编码

安装

npm

bash
npm install vue-router@4

yarn

bash
yarn add vue-router@4

基本使用

1. 定义路由组件

vue
<!-- Home.vue -->
<template>
  <div>
    <h1>首页</h1>
    <p>欢迎来到首页!</p>
  </div>
</template>

<!-- About.vue -->
<template>
  <div>
    <h1>关于我们</h1>
    <p>这是关于页面。</p>
  </div>
</template>

2. 创建路由实例

js
// 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

3. 在应用中使用路由

js
// 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')

4. 在模板中使用路由

vue
<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <router-link to="/">首页</router-link>
      <router-link to="/about">关于</router-link>
    </nav>
    <router-view />
  </div>
</template>

动态路由

路径参数

js
const routes = [
  // 动态路径参数 以冒号开头
  { path: '/user/:id', component: User }
]

在组件中获取参数:

vue
<template>
  <div>
    <h1>用户 {{ $route.params.id }}</h1>
  </div>
</template>

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

const route = useRoute()
console.log(route.params.id)
</script>

多个参数

js
const routes = [
  { path: '/user/:id/post/:postId', component: UserPost }
]

可选参数

js
const routes = [
  // ? 表示可选参数
  { path: '/user/:id?', component: User }
]

重复参数

js
const routes = [
  // + 表示一个或多个
  { path: '/user/:id+', component: User },
  // * 表示零个或多个
  { path: '/user/:id*', component: User }
]

嵌套路由

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

父组件模板:

vue
<!-- User.vue -->
<template>
  <div class="user">
    <h2>用户 {{ $route.params.id }}</h2>
    <router-view />
  </div>
</template>

编程式导航

router.push()

js
import { useRouter } from 'vue-router'

const router = useRouter()

// 字符串路径
router.push('/users/eduardo')

// 带有路径的对象
router.push({ path: '/users/eduardo' })

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })

router.replace()

js
// 替换当前位置,不会向 history 添加新记录
router.replace({ path: '/home' })

// 相当于
router.push({ path: '/home', replace: true })

router.go()

js
// 向前移动一条记录,与 router.forward() 相同
router.go(1)

// 返回一条记录,与 router.back() 相同
router.go(-1)

// 前进 3 条记录
router.go(3)

// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)

命名路由

js
const routes = [
  {
    path: '/user/:username',
    name: 'user',
    component: User
  }
]

使用命名路由:

vue
<template>
  <router-link :to="{ name: 'user', params: { username: 'erina' }}">
    用户
  </router-link>
</template>

命名视图

js
const routes = [
  {
    path: '/',
    components: {
      default: Home,
      // LeftSidebar: LeftSidebar 的缩写
      LeftSidebar,
      // 它们与 `<router-view>` 上的 `name` 属性匹配
      RightSidebar,
    },
  },
]

模板中使用:

vue
<template>
  <router-view class="view left-sidebar" name="LeftSidebar" />
  <router-view class="view main-content" />
  <router-view class="view right-sidebar" name="RightSidebar" />
</template>

路由守卫

全局前置守卫

js
router.beforeEach((to, from, next) => {
  // 检查用户是否已登录
  if (to.meta.requiresAuth && !isLoggedIn()) {
    // 重定向到登录页面
    next('/login')
  } else {
    next()
  }
})

全局后置钩子

js
router.afterEach((to, from) => {
  // 发送页面浏览统计
  sendToAnalytics(to.fullPath)
})

路由独享守卫

js
const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from, next) => {
      // 只对这个路由生效
      if (isValidUserId(to.params.id)) {
        next()
      } else {
        next('/404')
      }
    }
  }
]

组件内守卫

vue
<script setup>
import { onBeforeRouteEnter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'

// 在渲染该组件的对应路由被验证前调用
onBeforeRouteEnter((to, from, next) => {
  // 不能获取组件实例 `this` !
  // 因为当守卫执行前,组件实例还没被创建
  next()
})

// 在当前路由改变,但是该组件被复用时调用
onBeforeRouteUpdate((to, from, next) => {
  // 举例来说,对于一个带有动态参数的路径 `/users/:id`
  // 在 `/users/1` 和 `/users/2` 之间跳转的时候
  // 由于会渲染同样的 `UserDetails` 组件,所以组件实例会被复用
  // 而这个钩子就会在这个情况下被调用
  next()
})

// 在导航离开渲染该组件的对应路由时调用
onBeforeRouteLeave((to, from, next) => {
  // 通常用来预防用户在还未保存修改前突然离开
  const answer = window.confirm('你确定要离开吗?你的修改还没有保存!')
  if (answer) {
    next()
  } else {
    next(false)
  }
})
</script>

路由元信息

js
const routes = [
  {
    path: '/posts',
    component: PostsLayout,
    children: [
      {
        path: 'new',
        component: PostsNew,
        // 只有经过身份验证的用户才能创建帖子
        meta: { requiresAuth: true }
      },
      {
        path: ':id',
        component: PostsDetail,
        // 任何人都可以阅读帖子
        meta: { requiresAuth: false }
      }
    ]
  }
]

在守卫中使用:

js
router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!auth.loggedIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next()
  }
})

懒加载

js
// 路由级别的代码分割
const routes = [
  {
    path: '/about',
    name: 'About',
    // 懒加载
    component: () => import('../views/About.vue')
  },
  {
    path: '/admin',
    name: 'Admin',
    // 将组件分组到同一个异步块中
    component: () => import(/* webpackChunkName: "admin" */ '../views/Admin.vue')
  }
]

滚动行为

js
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 如果有保存的位置(浏览器前进/后退)
    if (savedPosition) {
      return savedPosition
    }
    
    // 如果有锚点
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth'
      }
    }
    
    // 默认滚动到顶部
    return { top: 0 }
  }
})

过渡效果

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>

数据获取

导航完成后获取

vue
<template>
  <div class="post">
    <div v-if="loading" class="loading">加载中...</div>
    <div v-if="error" class="error">{{ error }}</div>
    <div v-if="post" class="content">
      <h2>{{ post.title }}</h2>
      <p>{{ post.body }}</p>
    </div>
  </div>
</template>

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

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

const fetchData = async (id) => {
  error.value = null
  post.value = null
  loading.value = true
  
  try {
    const response = await fetch(`/api/posts/${id}`)
    post.value = await response.json()
  } catch (err) {
    error.value = err.toString()
  } finally {
    loading.value = false
  }
}

// 监听路由参数变化
watch(
  () => route.params.id,
  (newId) => {
    if (newId) {
      fetchData(newId)
    }
  },
  { immediate: true }
)
</script>

导航完成前获取

vue
<script setup>
import { onBeforeRouteEnter, onBeforeRouteUpdate } from 'vue-router'

const post = ref(null)
const error = ref(null)

const fetchData = async (id) => {
  try {
    const response = await fetch(`/api/posts/${id}`)
    return await response.json()
  } catch (err) {
    throw new Error('获取数据失败')
  }
}

onBeforeRouteEnter(async (to, from, next) => {
  try {
    const postData = await fetchData(to.params.id)
    next(() => {
      post.value = postData
    })
  } catch (err) {
    next(false) // 取消导航
  }
})

onBeforeRouteUpdate(async (to, from, next) => {
  if (to.params.id !== from.params.id) {
    try {
      post.value = await fetchData(to.params.id)
      next()
    } catch (err) {
      error.value = err.message
      next(false)
    }
  } else {
    next()
  }
})
</script>

最佳实践

1. 路由结构组织

js
// 按功能模块组织路由
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/auth',
    component: AuthLayout,
    children: [
      { path: 'login', component: Login },
      { path: 'register', component: Register },
      { path: 'forgot-password', component: ForgotPassword }
    ]
  },
  {
    path: '/dashboard',
    component: DashboardLayout,
    meta: { requiresAuth: true },
    children: [
      { path: '', component: DashboardHome },
      { path: 'profile', component: Profile },
      { path: 'settings', component: Settings }
    ]
  }
]

2. 路由守卫的使用

js
// 创建可复用的守卫函数
const requireAuth = (to, from, next) => {
  if (store.getters.isAuthenticated) {
    next()
  } else {
    next('/login')
  }
}

const requireGuest = (to, from, next) => {
  if (!store.getters.isAuthenticated) {
    next()
  } else {
    next('/dashboard')
  }
}

// 在路由中使用
const routes = [
  {
    path: '/login',
    component: Login,
    beforeEnter: requireGuest
  },
  {
    path: '/dashboard',
    component: Dashboard,
    beforeEnter: requireAuth
  }
]

3. 错误处理

js
// 全局错误处理
router.onError((error) => {
  console.error('路由错误:', error)
  
  // 发送错误到监控服务
  if (error.type === 'ChunkLoadError') {
    // 处理代码分割加载失败
    window.location.reload()
  }
})

// 404 处理
const routes = [
  // ... 其他路由
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
]

Vue Router 是构建 Vue.js 单页应用的核心工具,通过合理使用其功能,可以创建出用户体验良好、结构清晰的应用程序。

基于 Vue.js 官方文档构建的学习宝典