📋 完整流程概览

1
2
3
4
5
6
1. 预处理:扫描所有Vue组件文件 (asyncModules.ts)
2. 获取数据:从后端API获取路由配置 (getUserRoute)
3. 数据转换:将后端数据转换为Vue Router格式 (formatAsyncRoutes)
4. 组件映射:将字符串路径映射为实际组件 (transformComponentView)
5. 路由处理:多级路由扁平化处理 (flatMultiLevelRoutes)
6. 动态挂载:将路由添加到Vue Router实例 (router.addRoute)

🔧 第一步:组件预处理 (asyncModules.ts)

1
2
3
4
5
6
// src/router/asyncModules.ts
type ImportVueFileType = typeof import('*.vue')
type ImportVueFileFnType = () => Promise<ImportVueFileType>

// 🔍 使用 Vite 的 import.meta.glob 扫描所有 Vue 文件
const moduleFiles = import.meta.glob<ImportVueFileType>('@/views/**/*.vue')

这一步在做什么?

  • 扫描 src/views/ 目录下的所有 .vue 文件
  • 生成一个文件路径到动态导入函数的映射

生成的 moduleFiles 对象示例:

1
2
3
4
5
6
{
'/src/views/system/user/index.vue': () => import('/src/views/system/user/index.vue'),
'/src/views/system/role/index.vue': () => import('/src/views/system/role/index.vue'),
'/src/views/dashboard/workplace/index.vue': () => import('/src/views/dashboard/workplace/index.vue'),
// ... 更多文件
}

转换为 asyncRouteModules:

1
2
3
4
5
6
7
8
9
export const asyncRouteModules = Object.entries(moduleFiles).reduce((routes, [url, importFn]) => {
// 排除登录页和组件文件夹
if (!/\/(views\/login|components)\//.test(url)) {
// 转换路径:'/src/views/system/user/index.vue' → 'system/user/index'
const path = url.replace('/src/views/', '').replace('.vue', '')
routes[path] = importFn
}
return routes
}, {} as Recordable<ImportVueFileFnType>)

最终的 asyncRouteModules 对象:

1
2
3
4
5
6
{
'system/user/index': () => import('/src/views/system/user/index.vue'),
'system/role/index': () => import('/src/views/system/role/index.vue'),
'dashboard/workplace/index': () => import('/src/views/dashboard/workplace/index.vue'),
// ... 更多组件
}

🌐 第二步:获取后端数据

1
2
3
4
5
6
// src/stores/modules/route.ts
const generateRoutes = async (): Promise<RouteRecordRaw[]> => {
// 🔍 从后端API获取路由配置
const { data } = await getUserRoute()
// ...
}

后端返回的数据格式示例:

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
[
{
"id": "1",
"title": "系统管理",
"parentId": "0",
"type": 1, // 1=目录
"path": "/system",
"component": "Layout", // 🔑 关键:使用Layout作为父组件
"icon": "settings",
"isHidden": false,
"sort": 1,
"children": [
{
"id": "2",
"title": "用户管理",
"parentId": "1",
"type": 2, // 2=菜单
"path": "/system/user",
"component": "system/user/index", // 🔑 关键:对应实际组件路径
"icon": "user",
"isHidden": false,
"sort": 1
},
{
"id": "3",
"title": "角色管理",
"parentId": "1",
"type": 2,
"path": "/system/role",
"component": "system/role/index",
"icon": "role",
"isHidden": false,
"sort": 2
}
]
}
]

🔄 第三步:组件映射机制

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/stores/modules/route.ts
const layoutComponentMap = {
Layout: () => import('@/layout/index.vue'), // 布局组件
ParentView: () => import('@/components/ParentView/index.vue'), // 父级视图
}

/** 将component字符串转成真正的组件模块 */
const transformComponentView = (component: string) => {
// 🔍 首先检查是否是布局组件
return layoutComponentMap[component as keyof typeof layoutComponentMap]
// 🔍 否则从预扫描的组件中查找
|| asyncRouteModules[component]
}

映射过程示例:

1
2
3
4
// 映射示例:
transformComponentView('Layout') // → () => import('@/layout/index.vue')
transformComponentView('system/user/index') // → () => import('/src/views/system/user/index.vue')
transformComponentView('system/role/index') // → () => import('/src/views/system/role/index.vue')

🏗️ 第四步:数据格式化转换

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
const formatAsyncRoutes = (menus: RouteItem[]) => {
if (!menus.length) return []

const pathMap = new Map()
return mapTree(menus, (item) => {
pathMap.set(item.id, item.path)

// 🔍 子菜单排序
if (item.children?.length) {
item.children.sort((a, b) => (a?.sort ?? 0) - (b?.sort ?? 0))
}

// 🔍 处理激活菜单逻辑
if (item.parentId && item.type === 2 && item.permission) {
item.activeMenu = pathMap.get(item.parentId)
}

// 🔑 核心:转换为Vue Router格式
return {
path: item.path,
name: item.name ?? transformPathToName(item.path),
component: transformComponentView(item.component), // 🔍 组件映射
redirect: item.redirect,
meta: {
title: item.title,
hidden: item.isHidden,
keepAlive: item.isCache,
icon: item.icon,
showInTabs: item.showInTabs,
activeMenu: item.activeMenu,
},
}
}) as RouteRecordRaw[]
}

转换后的路由结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
{
path: '/system',
name: 'System',
component: () => import('@/layout/index.vue'), // 🔑 Layout作为父组件
meta: { title: '系统管理', icon: 'settings' },
children: [
{
path: '/system/user',
name: 'SystemUser',
component: () => import('/src/views/system/user/index.vue'), // 🔑 具体页面组件
meta: { title: '用户管理', icon: 'user' }
},
{
path: '/system/role',
name: 'SystemRole',
component: () => import('/src/views/system/role/index.vue'),
meta: { title: '角色管理', icon: 'role' }
}
]
}
]

📊 第五步:路由扁平化处理

1
2
3
4
5
6
7
8
9
10
11
12
/** 路由降级(把三级及其以上的路由转化为二级路由) */
export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => {
return cloneDeep(routes).map((route) => {
if (!isMultipleRoute(route)) return route

return {
...route,
// 🔍 将多级嵌套转为扁平结构
children: toTreeArray(route.children).map((item) => omit(item, 'children')) as RouteRecordRaw[],
}
})
}

为什么要扁平化?

  • Vue Router 推荐使用二级路由结构
  • 避免过深的嵌套导致的性能问题
  • 简化路由匹配逻辑

🚀 第六步:路由动态挂载

1
2
3
4
5
6
7
// src/router/guard.ts
const accessRoutes = await routeStore.generateRoutes()
accessRoutes.forEach((route) => {
if (!isHttp(route.path)) {
router.addRoute(route) // 🔍 动态添加到路由器
}
})

🎯 整个流程的关键优势

1. 自动化组件发现

  • 无需手动维护组件映射表
  • 新增页面只需放在正确目录即可

2. 类型安全

  • TypeScript 类型定义确保数据结构正确
  • 编译时就能发现路径错误

3. 权限控制

  • 后端根据用户角色返回不同路由
  • 前端只渲染有权限的页面

4. 按需加载

  • 使用动态导入实现代码分割
  • 只有访问时才加载对应组件

5. 灵活配置

  • 路由结构可在数据库中配置
  • 支持动态调整菜单结构和权限