Vue Router 路由管理
大约 9 分钟约 2804 字
Vue Router 路由管理
简介
Vue Router 是 Vue3 单页应用的官方路由管理方案,负责把浏览器地址、页面组件、导航守卫和权限控制组织成统一的页面跳转系统。它的核心价值不只是“页面切换”,而是帮助前端建立完整的导航模型:页面结构、布局嵌套、权限边界、懒加载、参数解析和页面状态恢复。
特点
实现
基础路由表与布局组织
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { title: '首页' }
},
{
path: 'about',
name: 'About',
component: () => import('@/views/About.vue'),
meta: { title: '关于' }
}
]
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录', noAuth: true }
},
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
meta: { requiresAuth: true, role: 'admin' },
children: [
{
path: 'users',
name: 'AdminUsers',
component: () => import('@/views/admin/Users.vue'),
meta: { title: '用户管理' }
},
{
path: 'settings',
name: 'AdminSettings',
component: () => import('@/views/admin/Settings.vue'),
meta: { title: '系统设置' }
}
]
},
{
path: '/product/:id',
name: 'ProductDetail',
component: () => import('@/views/ProductDetail.vue'),
props: true
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
]
export const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
return savedPosition ?? { top: 0 }
}
})常见后台布局结构:
- Layout
- Dashboard
- Users
- Settings
- Reports
嵌套路由的意义在于:
- 共享布局壳
- 保持左侧菜单和顶部导航稳定
- 只切换中间内容区路由守卫与权限控制
import type { Router } from 'vue-router'
export function setupRouterGuards(router: Router) {
router.beforeEach((to) => {
const token = localStorage.getItem('token')
const role = localStorage.getItem('role')
document.title = `${to.meta.title ?? 'MyApp'} - MyApp`
if (to.meta.noAuth) return true
if (to.meta.requiresAuth && !token) {
return {
name: 'Login',
query: { redirect: to.fullPath }
}
}
if (to.meta.role && to.meta.role !== role) {
return { name: 'Home' }
}
return true
})
router.afterEach((to) => {
console.log('route changed:', to.fullPath)
})
}// 登录成功后回跳
const redirect = route.query.redirect as string | undefined
router.replace(redirect || '/')路由守卫常见职责:
- 登录校验
- 角色权限校验
- 页面标题设置
- 埋点与访问日志
- 初始化页面级资源编程式导航与参数读取
import { useRouter, useRoute } from 'vue-router'
export function useProductNavigation() {
const router = useRouter()
const route = useRoute()
const goToProduct = (id: number) => {
router.push({ name: 'ProductDetail', params: { id } })
}
const goToSearch = (keyword: string) => {
router.push({ path: '/search', query: { q: keyword } })
}
const currentProductId = route.params.id
const currentKeyword = route.query.q
return {
goToProduct,
goToSearch,
currentProductId,
currentKeyword
}
}// 组件中通过 props 解耦 route.params
const propsRoute = {
path: '/orders/:id',
name: 'OrderDetail',
component: () => import('@/views/OrderDetail.vue'),
props: route => ({
id: Number(route.params.id),
from: route.query.from ?? 'list'
})
}动态路由与菜单联动
import type { RouteRecordRaw } from 'vue-router'
const dynamicRoutes: Record<string, RouteRecordRaw[]> = {
admin: [
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
children: [
{
path: 'roles',
name: 'AdminRoles',
component: () => import('@/views/admin/Roles.vue')
}
]
}
],
operator: [
{
path: '/workspace',
component: () => import('@/layouts/WorkLayout.vue'),
children: [
{
path: 'tasks',
name: 'WorkTasks',
component: () => import('@/views/workspace/Tasks.vue')
}
]
}
]
}
export function addDynamicRoutes(role: string) {
const routes = dynamicRoutes[role] || []
routes.forEach(route => router.addRoute(route))
}// 后端返回菜单时常见做法:
// 1. 登录成功拿到角色或菜单树
// 2. 前端转换为 RouteRecordRaw
// 3. addRoute 动态挂载
// 4. 刷新页面时重新恢复动态路由后端菜单转路由实战
// utils/routeHelper.ts — 后端菜单数据转 Vue Router 路由
import type { RouteRecordRaw } from 'vue-router'
import router from '@/router'
interface MenuItem {
id: number
name: string
path: string
component?: string
icon?: string
children?: MenuItem[]
meta?: {
title: string
hidden?: boolean
keepAlive?: boolean
roles?: string[]
}
}
// 动态导入视图组件的映射
const viewModules = import.meta.glob('@/views/**/*.vue')
function resolveComponent(componentPath: string) {
const path = `../views/${componentPath}.vue`
return viewModules[path] || (() => import('@/views/NotFound.vue'))
}
function transformMenuToRoutes(menus: MenuItem[]): RouteRecordRaw[] {
return menus
.filter(menu => !menu.meta?.hidden)
.map(menu => {
const route: RouteRecordRaw = {
path: menu.path,
name: menu.name,
meta: menu.meta || {},
}
if (menu.component) {
// 叶子节点 — 加载实际组件
route.component = resolveComponent(menu.component)
} else if (menu.children?.length) {
// 布局节点 — 递归处理子路由
route.component = () => import('@/layouts/AdminLayout.vue')
route.redirect = `${menu.path}/${menu.children[0].path}`
route.children = transformMenuToRoutes(menu.children)
}
return route
})
}
// 登录后动态加载路由
export async function loadDynamicRoutes() {
const token = localStorage.getItem('token')
if (!token) return
try {
const response = await fetch('/api/menus', {
headers: { Authorization: `Bearer ${token}` }
})
const menus: MenuItem[] = await response.json()
const routes = transformMenuToRoutes(menus)
routes.forEach(route => {
router.addRoute(route)
})
// 添加 404 兜底路由(必须在最后添加)
router.addRoute({
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
})
return routes
} catch (error) {
console.error('加载动态路由失败:', error)
}
}路由过渡动画
<!-- App.vue — 路由过渡动画 -->
<template>
<router-view v-slot="{ Component, route }">
<transition name="fade-slide" mode="out-in">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</template>
<style scoped>
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.3s ease;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(20px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(-20px);
}
</style><!-- 根据路由 meta 使用不同过渡效果 -->
<template>
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'" mode="out-in">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</template>
<style>
/* 淡入淡出 */
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* 缩放 */
.zoom-enter-active, .zoom-leave-active { transition: all 0.3s; }
.zoom-enter-from, .zoom-leave-to { transform: scale(0.95); opacity: 0; }
/* 滑动 */
.slide-enter-active, .slide-leave-active { transition: all 0.3s; }
.slide-enter-from { transform: translateY(20px); opacity: 0; }
.slide-leave-to { transform: translateY(-20px); opacity: 0; }
</style>keep-alive 与页面缓存协同
<template>
<router-view v-slot="{ Component, route }">
<keep-alive :include="['UserList', 'OrderList']">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</template>路由缓存适合:
- 列表筛选页
- 多 Tab 页面
- 回退时希望保留滚动位置和筛选条件的页面keep-alive 高级用法
<!-- 动态管理缓存列表 -->
<script setup lang="ts">
import { ref } from 'vue'
// 动态缓存列表
const cachedViews = ref<string[]>(['UserList', 'Dashboard'])
function addCachedView(name: string) {
if (!cachedViews.value.includes(name)) {
cachedViews.value.push(name)
}
}
function removeCachedView(name: string) {
const index = cachedViews.value.indexOf(name)
if (index > -1) cachedViews.value.splice(index, 1)
}
// 路由 meta 中配置是否缓存
// meta: { keepAlive: true }
</script>
<template>
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</template>// 配合 onActivated / onDeactivated 生命周期
// CachedList.vue
import { onActivated, onDeactivated, ref } from 'vue'
const scrollTop = ref(0)
onActivated(() => {
// 页面被激活时恢复滚动位置
window.scrollTo(0, scrollTop.value)
// 重新获取数据
fetchData()
})
onDeactivated(() => {
// 页面被缓存时记录滚动位置
scrollTop.value = window.scrollY
})路由元信息类型扩展
// types/router.d.ts — 扩展路由 meta 类型
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
title?: string // 页面标题
icon?: string // 菜单图标
roles?: string[] // 允许访问的角色
keepAlive?: boolean // 是否缓存
hidden?: boolean // 是否在菜单中隐藏
breadcrumb?: string[] // 面包屑
transition?: string // 过渡动画名称
activeMenu?: string // 选中菜单的路径(用于详情页高亮父菜单)
loading?: boolean // 页面加载状态
}
}
// 使用类型安全的 meta
const routes: RouteRecordRaw[] = [
{
path: '/admin/users',
component: () => import('@/views/admin/Users.vue'),
meta: {
title: '用户管理',
icon: 'users',
roles: ['admin', 'operator'],
keepAlive: true,
breadcrumb: ['首页', '系统管理', '用户管理'],
}
}
]路由懒加载与预加载
// 路由懒加载 — Webpack 魔法注释
const routes: RouteRecordRaw[] = [
{
path: '/admin',
component: () => import(/* webpackChunkName: "admin" */ '@/layouts/AdminLayout.vue'),
children: [
{
path: 'dashboard',
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Dashboard.vue'),
},
{
path: 'users',
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Users.vue'),
}
]
},
{
path: '/report',
component: () => import(/* webpackChunkName: "report" */ '@/views/Report.vue'),
}
]// 鼠标悬停时预加载页面组件
import { useRouter } from 'vue-router'
function usePrefetch() {
const router = useRouter()
const prefetched = new Set<string>()
function prefetchRoute(routeName: string) {
if (prefetched.has(routeName)) return
const route = router.resolve({ name: routeName })
if (route.matched.length > 0) {
const component = route.matched[0].components?.default
if (typeof component === 'function') {
component() // 触发动态导入
prefetched.add(routeName)
}
}
}
return { prefetchRoute }
}
// 在菜单组件中使用
// <a @mouseenter="prefetchRoute('AdminUsers')">用户管理</a>路由与 Pinia 状态同步
// stores/routeStore.ts — 管理标签页和路由历史
import { defineStore } from 'pinia'
import type { RouteLocationNormalized } from 'vue-router'
interface TagView {
path: string
name: string
title: string
query?: Record<string, string>
affix?: boolean // 固定标签
}
export const useRouteStore = defineStore('route', {
state: () => ({
cachedViews: [] as string[],
tagViews: [] as TagView[],
affixTags: [
{ path: '/dashboard', name: 'Dashboard', title: '首页', affix: true }
] as TagView[],
}),
actions: {
addTagView(route: RouteLocationNormalized) {
const exists = this.tagViews.some(tag => tag.path === route.path)
if (!exists) {
this.tagViews.push({
path: route.path,
name: route.name as string,
title: (route.meta?.title as string) || '未命名',
query: route.query as Record<string, string>,
})
}
},
removeTagView(path: string) {
const index = this.tagViews.findIndex(tag => tag.path === path)
if (index > -1) {
this.tagViews.splice(index, 1)
}
// 同时移除缓存
const view = this.tagViews.find(tag => tag.path === path)
if (view) {
const i = this.cachedViews.indexOf(view.name)
if (i > -1) this.cachedViews.splice(i, 1)
}
},
closeOtherTags(path: string) {
this.tagViews = this.tagViews.filter(
tag => tag.affix || tag.path === path
)
},
closeAllTags() {
this.tagViews = this.tagViews.filter(tag => tag.affix)
this.cachedViews = []
},
},
})路由错误处理
// 路由跳转错误捕获
router.onError((error) => {
console.error('路由错误:', error)
// 动态加载组件失败
if (error.message.includes('Failed to fetch dynamically imported module')) {
// 可以尝试重新加载页面
window.location.reload()
}
})
// 编程式导航错误处理
async function safeNavigate(name: string, params?: Record<string, string>) {
try {
await router.push({ name, params })
} catch (error: any) {
if (error.name === 'NavigationDuplicated') {
// 忽略重复导航
return
}
if (error.name === 'NavigationAborted') {
// 导航被守卫中止
console.warn('导航被中止')
return
}
console.error('导航错误:', error)
}
}
## 优点
- [x] 1.页面结构清晰 — 路由表天然就是页面导航图
- [x] 2.适合后台系统 — 菜单、布局、权限和页面切换配合自然
- [x] 3.懒加载友好 — 有利于首屏性能和分包策略
- [x] 4.与 Vue 生态一致 — Pinia、KeepAlive、异步组件等都能顺畅配合
## 缺点
- [x] 1.权限与动态路由容易复杂化 — 尤其是多角色、多租户场景
- [x] 2.前端状态和路由状态容易耦合过深 — 后期排障成本会上升
- [x] 3.SSR / SEO 场景要额外考虑 — SPA 路由不天然解决搜索引擎问题
- [x] 4.路由设计不当会拖累整个前端结构 — 页面越多越明显
## 总结
Vue Router 的重点不是“会跳页面”,而是能否把页面结构、权限边界、布局层次、参数读取和页面缓存组织成一个稳定的导航系统。真正成熟的项目,路由设计往往直接影响组件组织方式、菜单结构和后续维护成本。
## 关键知识点
- 路由表本质上是前端页面结构图。
- 嵌套路由适合解决布局壳与内容区分离问题。
- 守卫不仅用于鉴权,也常用于标题、埋点和初始化逻辑。
- 动态路由适合后台权限菜单,但要治理好恢复与清理逻辑。
## 项目落地视角
- 后台管理系统最需要先把 Layout、菜单、路由和权限模型理顺。
- 路由设计会直接影响页面拆分方式和组件层级。
- 列表页 / 详情页 / 编辑页切换经常伴随参数、缓存和回跳设计。
- 路由问题常常不是“跳不过去”,而是“状态、权限、缓存、参数”一起出问题。
## 常见误区
- 路由只是页面跳转工具,不把它当架构层设计。
- 所有页面都塞进同一级路由,后期越来越难维护。
- 动态路由只加不清,刷新和登出后状态混乱。
- 守卫里堆太多业务逻辑,导致跳转链路越来越重。
## 进阶路线
- 深入理解 Vue Router 与 Pinia、KeepAlive、权限菜单联动模式。
- 学习路由级懒加载、预加载和分包优化。
- 研究 SSR / SSG 场景下的路由协同。
- 在大型后台中沉淀菜单、权限、路由三者的统一配置模型。
## 适用场景
- Vue3 单页应用。
- 后台管理系统。
- 中大型业务前端项目。
- 需要菜单、权限、动态页面结构的系统。
## 落地建议
- 先设计页面结构和权限模型,再写路由表。
- 路由 meta 统一承载 title、权限、keepAlive 等配置。
- 动态路由要配合菜单和缓存恢复一起设计。
- 对复杂路由项目建立一份“页面结构图 + 路由图”。
## 排错清单
- 页面跳转异常:先看路由表和守卫返回值。
- 登录后回跳失败:检查 redirect 参数和 replace/push 使用。
- 动态菜单不显示:检查 addRoute 时机和角色恢复逻辑。
- 页面状态丢失:检查 keep-alive、query / params 与组件 key。
## 延伸阅读
- [Vue3 组合式 API](/dir/frontend/vue3_composition.md)
- [Pinia 状态管理](/dir/frontend/vue3_pinia.md)
- [Vue3 组件设计模式](/dir/frontend/vue3_component.md)
- [前端性能优化](/dir/frontend/frontend_performance.md)