Vue Router 高级技巧与路由管理最佳实践
Orion K Lv6

Vue Router 是 Vue.js 应用的核心路由库,掌握其高级特性对于构建复杂的单页应用至关重要。本文将深入探讨 Vue Router 4 的高级用法,包括动态路由、路由守卫、懒加载优化、路由元信息等实用技巧,帮助你构建更加健壮和高效的路由系统。

Vue Router 4 核心特性

1. 路由配置与基础设置

现代化路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'

// 路由懒加载函数
const lazyLoad = (view) => {
return () => import(`@/views/${view}.vue`)
}

// 路由配置
const routes = [
{
path: '/',
name: 'Home',
component: lazyLoad('Home'),
meta: {
title: '首页',
requiresAuth: false,
keepAlive: true
}
},
{
path: '/login',
name: 'Login',
component: lazyLoad('auth/Login'),
meta: {
title: '登录',
requiresAuth: false,
hideInMenu: true
}
},
{
path: '/dashboard',
name: 'Dashboard',
component: lazyLoad('Dashboard'),
meta: {
title: '仪表板',
requiresAuth: true,
roles: ['admin', 'user']
},
children: [
{
path: 'analytics',
name: 'Analytics',
component: lazyLoad('dashboard/Analytics'),
meta: {
title: '数据分析',
requiresAuth: true,
roles: ['admin']
}
},
{
path: 'profile',
name: 'Profile',
component: lazyLoad('dashboard/Profile'),
meta: {
title: '个人资料',
requiresAuth: true
}
}
]
},
{
path: '/users',
name: 'Users',
component: lazyLoad('users/UserList'),
meta: {
title: '用户管理',
requiresAuth: true,
roles: ['admin']
}
},
{
path: '/users/:id(\\d+)',
name: 'UserDetail',
component: lazyLoad('users/UserDetail'),
props: true,
meta: {
title: '用户详情',
requiresAuth: true
}
},
{
path: '/products',
name: 'Products',
component: lazyLoad('products/ProductLayout'),
redirect: '/products/list',
children: [
{
path: 'list',
name: 'ProductList',
component: lazyLoad('products/ProductList'),
meta: { title: '产品列表' }
},
{
path: 'create',
name: 'ProductCreate',
component: lazyLoad('products/ProductForm'),
meta: {
title: '创建产品',
requiresAuth: true,
roles: ['admin', 'editor']
}
},
{
path: ':id/edit',
name: 'ProductEdit',
component: lazyLoad('products/ProductForm'),
props: true,
meta: {
title: '编辑产品',
requiresAuth: true,
roles: ['admin', 'editor']
}
}
]
},
{
path: '/404',
name: 'NotFound',
component: lazyLoad('error/NotFound'),
meta: {
title: '页面未找到',
hideInMenu: true
}
},
{
path: '/:pathMatch(.*)*',
redirect: '/404'
}
]

// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
// 自定义滚动行为
if (savedPosition) {
return savedPosition
} else if (to.hash) {
return {
el: to.hash,
behavior: 'smooth'
}
} else {
return { top: 0 }
}
}
})

export default router

2. 动态路由管理

动态添加路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// utils/dynamicRoutes.js
import { useUserStore } from '@/stores/user'

// 动态路由配置
const dynamicRoutes = {
admin: [
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/admin/AdminLayout.vue'),
meta: {
title: '管理后台',
requiresAuth: true,
roles: ['admin']
},
children: [
{
path: 'users',
name: 'AdminUsers',
component: () => import('@/views/admin/UserManagement.vue'),
meta: { title: '用户管理' }
},
{
path: 'settings',
name: 'AdminSettings',
component: () => import('@/views/admin/SystemSettings.vue'),
meta: { title: '系统设置' }
}
]
}
],
editor: [
{
path: '/editor',
name: 'Editor',
component: () => import('@/views/editor/EditorLayout.vue'),
meta: {
title: '编辑器',
requiresAuth: true,
roles: ['editor', 'admin']
},
children: [
{
path: 'articles',
name: 'ArticleEditor',
component: () => import('@/views/editor/ArticleEditor.vue'),
meta: { title: '文章编辑' }
}
]
}
]
}

// 根据用户角色动态添加路由
export function addDynamicRoutes(router, userRoles) {
const routesToAdd = []

userRoles.forEach(role => {
if (dynamicRoutes[role]) {
routesToAdd.push(...dynamicRoutes[role])
}
})

routesToAdd.forEach(route => {
router.addRoute(route)
})

return routesToAdd
}

// 移除动态路由
export function removeDynamicRoutes(router, routeNames) {
routeNames.forEach(name => {
if (router.hasRoute(name)) {
router.removeRoute(name)
}
})
}

// 获取用户可访问的路由
export function getAccessibleRoutes(allRoutes, userRoles) {
return allRoutes.filter(route => {
if (!route.meta?.roles) return true
return route.meta.roles.some(role => userRoles.includes(role))
})
}

在应用中使用动态路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// stores/user.js
import { defineStore } from 'pinia'
import { addDynamicRoutes, removeDynamicRoutes } from '@/utils/dynamicRoutes'
import router from '@/router'

export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: localStorage.getItem('token'),
roles: [],
permissions: [],
addedRoutes: []
}),

getters: {
isLoggedIn: (state) => !!state.token,
hasRole: (state) => (role) => state.roles.includes(role),
hasPermission: (state) => (permission) => state.permissions.includes(permission)
},

actions: {
async login(credentials) {
try {
const response = await api.login(credentials)
const { user, token, roles, permissions } = response.data

this.user = user
this.token = token
this.roles = roles
this.permissions = permissions

localStorage.setItem('token', token)

// 动态添加路由
this.addedRoutes = addDynamicRoutes(router, roles)

return response
} catch (error) {
throw error
}
},

logout() {
// 移除动态路由
const routeNames = this.addedRoutes.map(route => route.name)
removeDynamicRoutes(router, routeNames)

// 清除状态
this.user = null
this.token = null
this.roles = []
this.permissions = []
this.addedRoutes = []

localStorage.removeItem('token')

// 跳转到登录页
router.push('/login')
},

async refreshUserInfo() {
try {
const response = await api.getUserInfo()
const { user, roles, permissions } = response.data

this.user = user
this.roles = roles
this.permissions = permissions

return response
} catch (error) {
this.logout()
throw error
}
}
}
})

3. 路由守卫详解

全局前置守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// router/guards.js
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

// 配置进度条
NProgress.configure({ showSpinner: false })

// 白名单路由(不需要登录)
const whiteList = ['/login', '/register', '/forgot-password', '/404']

export function setupRouterGuards(router) {
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 开始进度条
NProgress.start()

const userStore = useUserStore()

// 设置页面标题
if (to.meta?.title) {
document.title = `${to.meta.title} - 我的应用`
}

// 检查是否需要登录
if (to.meta?.requiresAuth) {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
next({
path: '/login',
query: { redirect: to.fullPath }
})
return
}

// 检查用户信息是否存在
if (!userStore.user) {
try {
await userStore.refreshUserInfo()
} catch (error) {
console.error('获取用户信息失败:', error)
next('/login')
return
}
}

// 检查角色权限
if (to.meta?.roles && to.meta.roles.length > 0) {
const hasRole = to.meta.roles.some(role => userStore.hasRole(role))
if (!hasRole) {
ElMessage.error('没有访问权限')
next('/403')
return
}
}

// 检查具体权限
if (to.meta?.permissions && to.meta.permissions.length > 0) {
const hasPermission = to.meta.permissions.some(permission =>
userStore.hasPermission(permission)
)
if (!hasPermission) {
ElMessage.error('没有操作权限')
next('/403')
return
}
}
}

// 已登录用户访问登录页,重定向到首页
if (to.path === '/login' && userStore.isLoggedIn) {
next('/')
return
}

next()
})

// 全局后置守卫
router.afterEach((to, from) => {
// 结束进度条
NProgress.done()

// 埋点统计
if (typeof gtag !== 'undefined') {
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: to.fullPath
})
}

// 记录路由访问日志
console.log(`路由跳转: ${from.fullPath} -> ${to.fullPath}`)
})

// 全局解析守卫
router.beforeResolve(async (to, from, next) => {
// 在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用

// 预加载数据
if (to.meta?.preload && typeof to.meta.preload === 'function') {
try {
await to.meta.preload(to, from)
} catch (error) {
console.error('预加载数据失败:', error)
// 可以选择继续导航或中断
}
}

next()
})
}

组件内守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!-- views/products/ProductDetail.vue -->
<template>
<div class="product-detail">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="product">
<h1>{{ product.name }}</h1>
<p>{{ product.description }}</p>
<div class="price">¥{{ product.price }}</div>

<div class="actions">
<button @click="editProduct" v-if="canEdit">编辑</button>
<button @click="deleteProduct" v-if="canDelete">删除</button>
</div>
</div>
<div v-else class="error">
产品不存在
</div>
</div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessageBox } from 'element-plus'

const route = useRoute()
const router = useRouter()
const userStore = useUserStore()

const product = ref(null)
const loading = ref(false)
const hasUnsavedChanges = ref(false)

// 权限检查
const canEdit = computed(() => {
return userStore.hasPermission('product:edit') ||
(product.value && product.value.createdBy === userStore.user?.id)
})

const canDelete = computed(() => {
return userStore.hasPermission('product:delete')
})

// 组件内前置守卫
async function beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
try {
const productData = await fetchProduct(to.params.id)
next(vm => {
vm.product = productData
})
} catch (error) {
next('/404')
}
}

// 路由更新守卫
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
loading.value = true
try {
product.value = await fetchProduct(to.params.id)
} catch (error) {
router.push('/404')
} finally {
loading.value = false
}
}
})

// 离开守卫
onBeforeRouteLeave(async (to, from) => {
if (hasUnsavedChanges.value) {
try {
await ElMessageBox.confirm(
'您有未保存的更改,确定要离开吗?',
'确认离开',
{
confirmButtonText: '离开',
cancelButtonText: '取消',
type: 'warning'
}
)
} catch {
return false // 取消导航
}
}
})

// 获取产品数据
async function fetchProduct(id) {
const response = await api.getProduct(id)
return response.data
}

// 编辑产品
function editProduct() {
router.push(`/products/${product.value.id}/edit`)
}

// 删除产品
async function deleteProduct() {
try {
await ElMessageBox.confirm('确定要删除这个产品吗?', '确认删除')
await api.deleteProduct(product.value.id)
router.push('/products')
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
}
}
}

// 初始化
onMounted(async () => {
if (!product.value) {
loading.value = true
try {
product.value = await fetchProduct(route.params.id)
} catch (error) {
router.push('/404')
} finally {
loading.value = false
}
}
})
</script>

4. 路由懒加载优化

分组懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// router/lazyLoad.js

// 按功能模块分组的懒加载
export const lazyLoadWithChunk = (chunkName) => {
return (componentName) => {
return () => import(
/* webpackChunkName: "[request]" */
`@/views/${componentName}.vue`
)
}
}

// 预加载关键路由
export const preloadRoutes = [
() => import('@/views/Dashboard.vue'),
() => import('@/views/Profile.vue')
]

// 路由配置示例
const routes = [
{
path: '/admin',
component: lazyLoadWithChunk('admin')('AdminLayout'),
children: [
{
path: 'users',
component: lazyLoadWithChunk('admin')('UserManagement')
},
{
path: 'settings',
component: lazyLoadWithChunk('admin')('Settings')
}
]
},
{
path: '/products',
component: lazyLoadWithChunk('products')('ProductLayout'),
children: [
{
path: 'list',
component: lazyLoadWithChunk('products')('ProductList')
},
{
path: 'detail/:id',
component: lazyLoadWithChunk('products')('ProductDetail')
}
]
}
]

// 预加载关键组件
export function preloadCriticalComponents() {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
preloadRoutes.forEach(loadComponent => {
loadComponent()
})
})
} else {
setTimeout(() => {
preloadRoutes.forEach(loadComponent => {
loadComponent()
})
}, 2000)
}
}

条件懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// utils/conditionalLazyLoad.js

// 根据用户角色条件加载
export function roleBasedLazyLoad(roles, componentPath) {
return async () => {
const userStore = useUserStore()

// 检查用户角色
const hasRequiredRole = roles.some(role => userStore.hasRole(role))

if (!hasRequiredRole) {
// 返回无权限组件
return import('@/components/NoPermission.vue')
}

// 返回实际组件
return import(`@/views/${componentPath}.vue`)
}
}

// 根据功能开关条件加载
export function featureFlagLazyLoad(featureFlag, componentPath, fallbackPath) {
return async () => {
const configStore = useConfigStore()

if (configStore.isFeatureEnabled(featureFlag)) {
return import(`@/views/${componentPath}.vue`)
} else {
return import(`@/views/${fallbackPath}.vue`)
}
}
}

// 使用示例
const routes = [
{
path: '/admin/advanced',
component: roleBasedLazyLoad(['admin'], 'admin/AdvancedSettings')
},
{
path: '/beta-feature',
component: featureFlagLazyLoad('betaFeatures', 'BetaFeature', 'ComingSoon')
}
]

5. 路由元信息与面包屑

面包屑组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<!-- components/Breadcrumb.vue -->
<template>
<nav class="breadcrumb" aria-label="breadcrumb">
<ol class="breadcrumb-list">
<li
v-for="(item, index) in breadcrumbItems"
:key="item.path"
class="breadcrumb-item"
:class="{ active: index === breadcrumbItems.length - 1 }"
>
<router-link
v-if="index < breadcrumbItems.length - 1 && item.path"
:to="item.path"
class="breadcrumb-link"
>
<i v-if="item.icon" :class="item.icon"></i>
{{ item.title }}
</router-link>
<span v-else class="breadcrumb-text">
<i v-if="item.icon" :class="item.icon"></i>
{{ item.title }}
</span>
<i
v-if="index < breadcrumbItems.length - 1"
class="breadcrumb-separator"
>
/
</i>
</li>
</ol>
</nav>
</template>

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

const route = useRoute()
const router = useRouter()

// 生成面包屑数据
const breadcrumbItems = computed(() => {
const matched = route.matched.filter(item => item.meta?.title)
const items = []

// 添加首页
items.push({
title: '首页',
path: '/',
icon: 'el-icon-house'
})

// 处理匹配的路由
matched.forEach((routeRecord, index) => {
const isLast = index === matched.length - 1
const item = {
title: routeRecord.meta.title,
path: isLast ? null : routeRecord.path,
icon: routeRecord.meta.icon
}

// 处理动态路由参数
if (routeRecord.meta.breadcrumbTitle) {
if (typeof routeRecord.meta.breadcrumbTitle === 'function') {
item.title = routeRecord.meta.breadcrumbTitle(route)
} else {
item.title = routeRecord.meta.breadcrumbTitle
}
}

items.push(item)
})

return items
})
</script>

<style scoped>
.breadcrumb {
padding: 12px 0;
font-size: 14px;
}

.breadcrumb-list {
display: flex;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
}

.breadcrumb-item {
display: flex;
align-items: center;
}

.breadcrumb-link {
color: #606266;
text-decoration: none;
transition: color 0.2s;
}

.breadcrumb-link:hover {
color: #409eff;
}

.breadcrumb-text {
color: #303133;
}

.breadcrumb-separator {
margin: 0 8px;
color: #c0c4cc;
font-style: normal;
}

.breadcrumb-item.active .breadcrumb-text {
font-weight: 500;
}
</style>

路由元信息配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 扩展的路由配置
const routes = [
{
path: '/users/:id',
name: 'UserDetail',
component: () => import('@/views/users/UserDetail.vue'),
meta: {
title: '用户详情',
breadcrumbTitle: (route) => `用户 #${route.params.id}`,
icon: 'el-icon-user',
requiresAuth: true,
roles: ['admin', 'hr'],
permissions: ['user:view'],
keepAlive: true,

// 页面配置
layout: 'DefaultLayout',
sidebar: true,
header: true,

// SEO 配置
description: '查看用户详细信息',
keywords: ['用户', '详情', '管理'],

// 缓存配置
cache: {
key: (route) => `user-${route.params.id}`,
ttl: 300000 // 5分钟
},

// 预加载数据
preload: async (to, from) => {
const userStore = useUserStore()
await userStore.fetchUser(to.params.id)
}
}
}
]

6. 路由状态管理

路由状态 Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// stores/router.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useRouterStore = defineStore('router', () => {
// 状态
const visitedViews = ref([])
const cachedViews = ref([])
const currentRoute = ref(null)
const routeHistory = ref([])

// 计算属性
const hasVisitedViews = computed(() => visitedViews.value.length > 0)
const canGoBack = computed(() => routeHistory.value.length > 1)

// 添加访问过的视图
function addVisitedView(view) {
if (visitedViews.value.some(v => v.path === view.path)) return

visitedViews.value.push({
name: view.name,
path: view.path,
title: view.meta?.title || 'Unknown',
meta: view.meta
})
}

// 删除访问过的视图
function delVisitedView(view) {
const index = visitedViews.value.findIndex(v => v.path === view.path)
if (index > -1) {
visitedViews.value.splice(index, 1)
}
}

// 删除其他视图
function delOthersVisitedViews(view) {
visitedViews.value = visitedViews.value.filter(v => {
return v.meta?.affix || v.path === view.path
})
}

// 删除所有视图
function delAllVisitedViews() {
visitedViews.value = visitedViews.value.filter(v => v.meta?.affix)
}

// 添加缓存视图
function addCachedView(view) {
if (cachedViews.value.includes(view.name)) return
if (view.meta?.keepAlive) {
cachedViews.value.push(view.name)
}
}

// 删除缓存视图
function delCachedView(view) {
const index = cachedViews.value.indexOf(view.name)
if (index > -1) {
cachedViews.value.splice(index, 1)
}
}

// 更新当前路由
function updateCurrentRoute(route) {
currentRoute.value = route

// 添加到历史记录
if (routeHistory.value[routeHistory.value.length - 1]?.path !== route.path) {
routeHistory.value.push({
path: route.path,
name: route.name,
timestamp: Date.now()
})

// 限制历史记录长度
if (routeHistory.value.length > 50) {
routeHistory.value.shift()
}
}
}

// 获取上一个路由
function getPreviousRoute() {
if (routeHistory.value.length < 2) return null
return routeHistory.value[routeHistory.value.length - 2]
}

return {
// 状态
visitedViews,
cachedViews,
currentRoute,
routeHistory,

// 计算属性
hasVisitedViews,
canGoBack,

// 方法
addVisitedView,
delVisitedView,
delOthersVisitedViews,
delAllVisitedViews,
addCachedView,
delCachedView,
updateCurrentRoute,
getPreviousRoute
}
})

标签页组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<!-- components/TabsView.vue -->
<template>
<div class="tabs-view">
<div class="tabs-container">
<div
v-for="view in visitedViews"
:key="view.path"
class="tab-item"
:class="{ active: isActive(view) }"
@click="goToView(view)"
@contextmenu.prevent="openContextMenu($event, view)"
>
<span class="tab-title">{{ view.title }}</span>
<i
v-if="!view.meta?.affix"
class="tab-close el-icon-close"
@click.stop="closeTab(view)"
></i>
</div>
</div>

<!-- 右键菜单 -->
<div
v-if="contextMenu.visible"
class="context-menu"
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
>
<div class="menu-item" @click="refreshTab">刷新</div>
<div class="menu-item" @click="closeCurrentTab">关闭</div>
<div class="menu-item" @click="closeOtherTabs">关闭其他</div>
<div class="menu-item" @click="closeAllTabs">关闭所有</div>
</div>
</div>
</template>

<script setup>
import { reactive, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRouterStore } from '@/stores/router'

const route = useRoute()
const router = useRouter()
const routerStore = useRouterStore()

const { visitedViews } = storeToRefs(routerStore)

const contextMenu = reactive({
visible: false,
x: 0,
y: 0,
currentView: null
})

// 判断是否为当前激活的标签
function isActive(view) {
return view.path === route.path
}

// 跳转到指定视图
function goToView(view) {
router.push(view.path)
}

// 关闭标签
function closeTab(view) {
routerStore.delVisitedView(view)
routerStore.delCachedView(view)

// 如果关闭的是当前标签,跳转到最后一个标签
if (isActive(view)) {
const lastView = visitedViews.value[visitedViews.value.length - 1]
if (lastView) {
router.push(lastView.path)
} else {
router.push('/')
}
}
}

// 打开右键菜单
function openContextMenu(event, view) {
contextMenu.visible = true
contextMenu.x = event.clientX
contextMenu.y = event.clientY
contextMenu.currentView = view
}

// 关闭右键菜单
function closeContextMenu() {
contextMenu.visible = false
contextMenu.currentView = null
}

// 刷新标签
function refreshTab() {
const view = contextMenu.currentView
routerStore.delCachedView(view)

nextTick(() => {
router.replace({
path: '/redirect' + view.path
})
})

closeContextMenu()
}

// 关闭当前标签
function closeCurrentTab() {
closeTab(contextMenu.currentView)
closeContextMenu()
}

// 关闭其他标签
function closeOtherTabs() {
routerStore.delOthersVisitedViews(contextMenu.currentView)
closeContextMenu()
}

// 关闭所有标签
function closeAllTabs() {
routerStore.delAllVisitedViews()
router.push('/')
closeContextMenu()
}

// 监听点击事件关闭右键菜单
function handleClickOutside() {
closeContextMenu()
}

onMounted(() => {
document.addEventListener('click', handleClickOutside)
})

onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>

7. 路由性能监控

路由性能监控工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// utils/routePerformance.js
class RoutePerformanceMonitor {
constructor() {
this.metrics = new Map()
this.observers = []
}

// 开始监控路由
startMonitoring(router) {
router.beforeEach((to, from, next) => {
this.startRouteTimer(to.path)
next()
})

router.afterEach((to, from) => {
this.endRouteTimer(to.path)
this.recordNavigation(to, from)
})

// 监控组件加载时间
this.observeComponentLoading()
}

// 开始路由计时
startRouteTimer(path) {
if (!this.metrics.has(path)) {
this.metrics.set(path, {
loadTimes: [],
componentLoadTimes: [],
navigationCount: 0,
errors: []
})
}

this.metrics.get(path).startTime = performance.now()
}

// 结束路由计时
endRouteTimer(path) {
const metric = this.metrics.get(path)
if (metric && metric.startTime) {
const loadTime = performance.now() - metric.startTime
metric.loadTimes.push(loadTime)
metric.navigationCount++

// 如果加载时间过长,记录警告
if (loadTime > 3000) {
console.warn(`路由 ${path} 加载时间过长: ${loadTime.toFixed(2)}ms`)
}

delete metric.startTime
}
}

// 记录导航信息
recordNavigation(to, from) {
const navigation = {
from: from.path,
to: to.path,
timestamp: Date.now(),
userAgent: navigator.userAgent
}

// 发送到分析服务
this.sendToAnalytics('route_navigation', navigation)
}

// 监控组件加载
observeComponentLoading() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name.includes('chunk')) {
console.log(`组件加载: ${entry.name}, 耗时: ${entry.duration.toFixed(2)}ms`)
}
})
})

observer.observe({ entryTypes: ['resource'] })
this.observers.push(observer)
}
}

// 获取路由性能报告
getPerformanceReport() {
const report = {}

this.metrics.forEach((metric, path) => {
const loadTimes = metric.loadTimes
if (loadTimes.length > 0) {
report[path] = {
averageLoadTime: loadTimes.reduce((a, b) => a + b, 0) / loadTimes.length,
minLoadTime: Math.min(...loadTimes),
maxLoadTime: Math.max(...loadTimes),
navigationCount: metric.navigationCount,
errorCount: metric.errors.length
}
}
})

return report
}

// 发送数据到分析服务
sendToAnalytics(event, data) {
// 实现发送逻辑
if (typeof gtag !== 'undefined') {
gtag('event', event, data)
}
}

// 清理监控器
destroy() {
this.observers.forEach(observer => observer.disconnect())
this.metrics.clear()
}
}

export const routePerformanceMonitor = new RoutePerformanceMonitor()

总结

Vue Router 4 提供了强大而灵活的路由管理能力,通过合理运用这些高级特性,可以构建出高性能、用户体验良好的单页应用。关键要点包括:

  1. 动态路由管理:根据用户权限动态添加和移除路由
  2. 路由守卫:实现细粒度的权限控制和导航管理
  3. 懒加载优化:按需加载组件,提升应用性能
  4. 路由元信息:丰富的路由配置,支持面包屑、权限等功能
  5. 状态管理:结合 Pinia 管理路由相关状态
  6. 性能监控:监控路由性能,优化用户体验

在实际项目中,应该根据应用的复杂度和需求选择合适的路由策略,避免过度设计。同时,要注意路由的可维护性和可扩展性,为未来的功能扩展留出空间。

本站由 提供部署服务