Vue Router路由管理
Vue Router是Vue.js的官方路由管理器,用于构建单页应用(SPA)。它与Vue.js深度集成,提供了声明式的路由配置、动态路由匹配、嵌套路由、导航守卫等强大功能。
核心价值
Vue Router优势
- 🧭 声明式路由:通过配置定义应用路由结构
- 🔄 动态路由:支持参数化路由和通配符
- 🧩 组件化路由:每个路由对应一个组件
- 🛡️ 路由守卫:控制路由的访问权限
- 📱 响应式路由:与Vue响应式系统完美结合
1. Vue Router基础配置
1.1 安装与基本设置
bash
1# 安装Vue Router2npm install vue-router@4 # 适用于Vue 3在Vue 3项目中创建路由配置:
src/router/index.js
js
1import { createRouter, createWebHistory } from 'vue-router'2import HomeView from '../views/HomeView.vue'3import AboutView from '../views/AboutView.vue'4import NotFound from '../views/NotFound.vue'56const routes = [7 {8 path: '/',9 name: 'home',10 component: HomeView,11 meta: {12 title: '首页',13 requiresAuth: false14 }15 },16 {17 path: '/about',18 name: 'about',19 component: AboutView,20 meta: {21 title: '关于我们',22 requiresAuth: false23 }24 },25 {26 path: '/user/:id',27 name: 'user',28 component: () => import('../views/UserView.vue'), // 懒加载路由29 meta: {30 title: '用户详情',31 requiresAuth: true32 }33 },34 {35 path: '/:pathMatch(.*)*',36 name: 'not-found',37 component: NotFound,38 meta: {39 title: '页面未找到'40 }41 }42]4344const router = createRouter({45 history: createWebHistory(), // HTML5 History模式46 routes,47 scrollBehavior(to, from, savedPosition) {48 // 页面切换时滚动行为49 if (savedPosition) {50 return savedPosition // 如果是前进/后退按钮,恢复之前的位置51 } else {52 return { top: 0 } // 否则滚动到顶部53 }54 }55})5657export default router在主应用中注册路由:
src/main.js
js
1import { createApp } from 'vue'2import App from './App.vue'3import router from './router'45const app = createApp(App)6app.use(router)7app.mount('#app')1.2 路由视图与链接
在应用中使用路由导航和视图:
App.vue
vue
1<template>2 <header>3 <nav>4 <RouterLink to="/">首页</RouterLink>5 <RouterLink to="/about">关于</RouterLink>6 <RouterLink :to="{ name: 'user', params: { id: userId } }">用户资料</RouterLink>7 </nav>8 </header>9 10 <!-- 路由视图,路由匹配的组件会渲染在这里 -->11 <RouterView v-slot="{ Component }">12 <Transition name="fade" mode="out-in">13 <component :is="Component" />14 </Transition>15 </RouterView>16</template>1718<script setup>19import { RouterLink, RouterView } from 'vue-router'20import { ref } from 'vue'2122const userId = ref('12345')23</script>2425<style scoped>26nav {27 padding: 1rem 0;28}2930nav a {31 margin-right: 1rem;32}3334.fade-enter-active,35.fade-leave-active {36 transition: opacity 0.3s;37}3839.fade-enter-from,40.fade-leave-to {41 opacity: 0;42}43</style>2. 高级路由功能
2.1 嵌套路由
嵌套路由允许在父路由组件内部定义子路由,实现更复杂的UI结构。
嵌套路由配置示例
js
1const routes = [2 {3 path: '/dashboard',4 component: DashboardLayout,5 children: [6 {7 path: '', // 默认子路由,访问/dashboard时显示8 name: 'dashboard-overview',9 component: DashboardOverview10 },11 {12 path: 'profile',13 name: 'dashboard-profile',14 component: UserProfile15 },16 {17 path: 'settings',18 name: 'dashboard-settings',19 component: UserSettings20 }21 ]22 }23]使用嵌套路由的父组件需要包含<RouterView>来显示子路由组件:
DashboardLayout.vue
vue
1<template>2 <div class="dashboard-layout">3 <aside class="sidebar">4 <nav>5 <RouterLink :to="{ name: 'dashboard-overview' }">概览</RouterLink>6 <RouterLink :to="{ name: 'dashboard-profile' }">个人资料</RouterLink>7 <RouterLink :to="{ name: 'dashboard-settings' }">设置</RouterLink>8 </nav>9 </aside>10 11 <main class="content">12 <!-- 子路由组件将在这里渲染 -->13 <RouterView />14 </main>15 </div>16</template>2.2 动态路由匹配
动态路由支持在URL中包含参数,并在组件中访问这些参数:
UserView.vue
vue
1<template>2 <div class="user-view">3 <h1>用户详情</h1>4 <div v-if="user">5 <img :src="user.avatar" alt="User Avatar" />6 <h2>{{ user.name }}</h2>7 <p>ID: {{ $route.params.id }}</p>8 <p>邮箱: {{ user.email }}</p>9 </div>10 <p v-else>加载用户数据中...</p>11 </div>12</template>1314<script setup>15import { ref, onMounted, watch } from 'vue'16import { useRoute } from 'vue-router'1718const route = useRoute()19const user = ref(null)2021// 根据路由参数加载用户数据22const fetchUserData = async (userId) => {23 try {24 // 模拟API调用25 const response = await fetch(`/api/users/${userId}`)26 if (!response.ok) throw new Error('用户数据获取失败')27 user.value = await response.json()28 } catch (error) {29 console.error(error)30 }31}3233// 组件挂载时获取数据34onMounted(() => {35 fetchUserData(route.params.id)36})3738// 当路由参数变化时重新获取数据39watch(() => route.params.id, (newId) => {40 fetchUserData(newId)41})42</script>2.3 路由懒加载
路由懒加载可以提高应用性能,只有当路由被访问时才加载对应的组件:
js
1const routes = [2 {3 path: '/analytics',4 name: 'analytics',5 // 使用import动态导入实现懒加载6 component: () => import('../views/AnalyticsView.vue'),7 // 可以自定义加载中和错误状态的组件8 meta: {9 title: '数据分析'10 }11 },12 {13 path: '/reports',14 name: 'reports',15 component: () => import(/* webpackChunkName: "reports" */ '../views/ReportsView.vue')16 // webpackChunkName可以指定打包后的文件名17 }18]2.4 命名视图
命名视图允许在同一个路由下显示多个组件,适用于复杂的布局:
js
1const routes = [2 {3 path: '/workspace',4 components: {5 default: WorkspaceMain,6 sidebar: WorkspaceSidebar,7 toolbar: WorkspaceToolbar8 }9 }10]在模板中使用命名视图:
vue
1<template>2 <div class="layout">3 <RouterView name="toolbar" class="toolbar" />4 <RouterView name="sidebar" class="sidebar" />5 <RouterView class="main-content" /> <!-- default视图 -->6 </div>7</template>3. 导航守卫
3.1 全局导航守卫
全局导航守卫可以用于权限验证、页面标题设置等功能:
src/router/index.js
js
1// 全局前置守卫2router.beforeEach((to, from, next) => {3 // 设置文档标题4 document.title = to.meta.title || '默认标题'5 6 // 权限检查7 if (to.meta.requiresAuth) {8 const authService = useAuthService()9 if (!authService.isLoggedIn()) {10 // 未登录时重定向到登录页11 next({12 path: '/login',13 query: { redirect: to.fullPath } // 保存原目标路径14 })15 return16 }17 }18 19 // 继续导航20 next()21})2223// 全局解析守卫24router.beforeResolve(async (to, from, next) => {25 try {26 // 在导航被确认前,加载与视图相关的数据27 for (const component of to.matched) {28 if (component.meta.fetchData) {29 await component.meta.fetchData()30 }31 }32 next()33 } catch (error) {34 console.error('数据加载失败', error)35 next(false) // 中止导航36 }37})3839// 全局后置钩子40router.afterEach((to, from) => {41 // 导航完成后的逻辑42 const analyticsService = useAnalyticsService()43 analyticsService.trackPageView(to.fullPath)44})3.2 路由独享守卫
可以为特定路由配置守卫:
js
1const routes = [2 {3 path: '/admin',4 component: AdminPanel,5 beforeEnter: (to, from, next) => {6 const authService = useAuthService()7 if (!authService.hasAdminRights()) {8 next('/forbidden')9 } else {10 next()11 }12 }13 }14]3.3 组件内守卫
可以在组件内部定义路由守卫:
vue
1<script setup>2import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'3import { ref } from 'vue'45const hasUnsavedChanges = ref(false)67// 离开路由前确认8onBeforeRouteLeave((to, from, next) => {9 if (hasUnsavedChanges.value) {10 if (confirm('有未保存的更改,确定要离开吗?')) {11 next()12 } else {13 next(false)14 }15 } else {16 next()17 }18})1920// 路由参数更新时的处理21onBeforeRouteUpdate(async (to, from, next) => {22 if (to.params.id !== from.params.id) {23 try {24 // 加载新的数据25 await loadData(to.params.id)26 next()27 } catch (error) {28 console.error('数据加载失败', error)29 next(false)30 }31 } else {32 next()33 }34})35</script>4. 路由模式与历史记录
Vue Router支持多种路由模式:
js
1// HTML5 History模式2const router = createRouter({3 history: createWebHistory(),4 routes5})67// Hash模式8const router = createRouter({9 history: createWebHashHistory(),10 routes11})1213// 内存模式(用于SSR等场景)14const router = createRouter({15 history: createMemoryHistory(),16 routes17})4.1 History模式配置
History模式需要服务器端配置支持,以防止刷新页面时出现404错误:
- Nginx配置
- Apache配置
- Express.js配置
nginx
1server {2 listen 80;3 server_name example.com;4 root /var/www/html;5 index index.html;67 location / {8 try_files $uri $uri/ /index.html;9 }10}apache
1<IfModule mod_rewrite.c>2 RewriteEngine On3 RewriteBase /4 RewriteRule ^index\.html$ - [L]5 RewriteCond %{REQUEST_FILENAME} !-f6 RewriteCond %{REQUEST_FILENAME} !-d7 RewriteRule . /index.html [L]8</IfModule>js
1const express = require('express')2const app = express()34// 静态资源5app.use(express.static('dist'))67// 所有路由指向index.html8app.get('*', (req, res) => {9 res.sendFile('dist/index.html', { root: __dirname })10})1112app.listen(3000)5. 路由最佳实践
5.1 模块化路由配置
对于大型应用,可以将路由配置模块化:
src/router/modules/user.js
js
1export default [2 {3 path: '/users',4 component: () => import('@/views/users/UserList.vue'),5 meta: { title: '用户列表' }6 },7 {8 path: '/users/:id',9 component: () => import('@/views/users/UserDetail.vue'),10 meta: { title: '用户详情' }11 },12 // 其他用户相关路由...13]src/router/index.js
js
1import { createRouter, createWebHistory } from 'vue-router'2import userRoutes from './modules/user'3import productRoutes from './modules/product'4import orderRoutes from './modules/order'56const routes = [7 {8 path: '/',9 component: () => import('@/views/Home.vue'),10 meta: { title: '首页' }11 },12 // 导入各模块路由13 ...userRoutes,14 ...productRoutes,15 ...orderRoutes,16 {17 path: '/:pathMatch(.*)*',18 component: () => import('@/views/NotFound.vue'),19 meta: { title: '页面未找到' }20 }21]2223const router = createRouter({24 history: createWebHistory(),25 routes26})2728export default router5.2 路由元信息的高级用法
路由元信息可以存储与路由相关的自定义数据:
js
1const routes = [2 {3 path: '/dashboard',4 component: Dashboard,5 meta: {6 title: '仪表盘',7 icon: 'dashboard',8 requiresAuth: true,9 permissions: ['view-dashboard'],10 layout: 'admin',11 transition: 'fade',12 breadcrumb: [{ name: '首页', path: '/' }, { name: '仪表盘' }],13 cacheable: true14 }15 }16]使用路由元信息构建导航菜单:
SideMenu.vue
vue
1<template>2 <nav class="side-menu">3 <ul>4 <li v-for="route in navigationRoutes" :key="route.path">5 <RouterLink :to="route.path">6 <i :class="route.meta.icon"></i>7 {{ route.meta.title }}8 </RouterLink>9 </li>10 </ul>11 </nav>12</template>1314<script setup>15import { computed } from 'vue'16import { useRouter } from 'vue-router'1718const router = useRouter()1920// 过滤出需要在导航中显示的路由21const navigationRoutes = computed(() => {22 return router.options.routes.filter(route => {23 return route.meta?.showInNav && hasPermission(route.meta.permissions)24 })25})2627const hasPermission = (requiredPermissions) => {28 // 权限检查逻辑29 const userPermissions = getCurrentUserPermissions()30 if (!requiredPermissions) return true31 return requiredPermissions.some(perm => userPermissions.includes(perm))32}33</script>5.3 路由权限管理
实现基于角色的路由权限控制:
src/permission.js
js
1import router from './router'2import store from './store'34// 白名单路由(无需登录)5const whiteList = ['/login', '/register', '/forgot-password']67router.beforeEach(async (to, from, next) => {8 // 获取用户令牌9 const hasToken = localStorage.getItem('token')1011 if (hasToken) {12 if (to.path === '/login') {13 // 已登录则重定向到首页14 next({ path: '/' })15 } else {16 // 判断用户是否已获取角色信息17 const hasRoles = store.getters.roles && store.getters.roles.length > 018 if (hasRoles) {19 next()20 } else {21 try {22 // 获取用户信息(包括角色)23 const { roles } = await store.dispatch('user/getUserInfo')24 25 // 根据角色生成可访问路由26 const accessibleRoutes = await store.dispatch('permission/generateRoutes', roles)27 28 // 动态添加路由29 accessibleRoutes.forEach(route => {30 router.addRoute(route)31 })32 33 // 确保addRoute完成34 next({ ...to, replace: true })35 } catch (error) {36 // 出错则重置令牌并重定向到登录页37 await store.dispatch('user/resetToken')38 next(`/login?redirect=${to.path}`)39 }40 }41 }42 } else {43 // 未登录44 if (whiteList.includes(to.path)) {45 // 在白名单中,直接访问46 next()47 } else {48 // 否则重定向到登录页49 next(`/login?redirect=${to.path}`)50 }51 }52})6. 与状态管理结合
Vue Router与Pinia结合使用示例:
src/stores/routerStore.js
js
1import { defineStore } from 'pinia'2import { useRouter, useRoute } from 'vue-router'34export const useRouterStore = defineStore('router', () => {5 const router = useRouter()6 const route = useRoute()7 8 // 导航历史9 const navigationHistory = ref([])10 11 // 添加历史记录12 function addToHistory(route) {13 // 避免重复14 if (!navigationHistory.value.find(item => item.path === route.path)) {15 navigationHistory.value.push({16 path: route.path,17 name: route.name,18 title: route.meta.title,19 timestamp: Date.now()20 })21 22 // 限制历史记录数量23 if (navigationHistory.value.length > 10) {24 navigationHistory.value.shift()25 }26 }27 }28 29 // 导航到上一个页面30 function goBack() {31 if (navigationHistory.value.length > 1) {32 // 移除当前页面33 navigationHistory.value.pop()34 // 获取上一个页面35 const prevPage = navigationHistory.value[navigationHistory.value.length - 1]36 router.push(prevPage.path)37 } else {38 router.push('/')39 }40 }41 42 // 快捷导航方法43 function navigateTo(routeName, params = {}, query = {}) {44 router.push({ name: routeName, params, query })45 }46 47 // 跟踪当前路由48 const currentRouteMeta = computed(() => route.meta)49 const currentPath = computed(() => route.path)50 const isActiveRoute = (path) => route.path === path51 52 return { 53 navigationHistory,54 addToHistory,55 goBack,56 navigateTo,57 currentRouteMeta,58 currentPath,59 isActiveRoute60 }61})在组件中使用:
vue
1<template>2 <div>3 <button @click="routerStore.goBack()">返回上一页</button>4 <button @click="routerStore.navigateTo('dashboard')">前往仪表盘</button>5 6 <div v-if="routerStore.navigationHistory.length > 0">7 <h3>浏览历史</h3>8 <ul>9 <li v-for="(item, index) in routerStore.navigationHistory" :key="index">10 {{ item.title || item.name }} - {{ new Date(item.timestamp).toLocaleTimeString() }}11 </li>12 </ul>13 </div>14 </div>15</template>1617<script setup>18import { useRouterStore } from '../stores/routerStore'1920const routerStore = useRouterStore()21</script>7. Vue Router与SSR
Vue Router在服务器端渲染(SSR)环境中的使用:
server.js (基于Express和Vue SSR)
js
1const express = require('express')2const { renderToString } = require('vue/server-renderer')3const { createSSRApp } = require('vue')4const { createRouter, createMemoryHistory } = require('vue-router')5const fs = require('fs')6const path = require('path')78const server = express()910server.get('*', async (req, res) => {11 try {12 // 1. 为每个请求创建新的Vue应用实例13 const app = createSSRApp(/* ... */)14 15 // 2. 使用内存历史模式创建router16 const router = createRouter({17 history: createMemoryHistory(),18 routes: [/* 路由配置 */]19 })20 21 app.use(router)22 23 // 3. 将请求的URL设置为路由位置24 await router.push(req.url)25 await router.isReady()26 27 // 4. 渲染应用为HTML28 const appContent = await renderToString(app)29 30 // 5. 注入渲染的HTML到模板中31 const html = fs.readFileSync(32 path.join(__dirname, 'index.html'),33 'utf-8'34 ).replace('<!--app-html-->', appContent)35 36 // 6. 返回页面37 res.status(200).set({ 'Content-Type': 'text/html' }).end(html)38 } catch (error) {39 console.error(error)40 res.status(500).end('Internal Server Error')41 }42})4344server.listen(3000)
参与讨论