📋 完整流程概览
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
| type ImportVueFileType = typeof import('*.vue') type ImportVueFileFnType = () => Promise<ImportVueFileType>
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)) { 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
| const generateRoutes = async (): Promise<RouteRecordRaw[]> => { 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, "path": "/system", "component": "Layout", "icon": "settings", "isHidden": false, "sort": 1, "children": [ { "id": "2", "title": "用户管理", "parentId": "1", "type": 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
| const layoutComponentMap = { Layout: () => import('@/layout/index.vue'), ParentView: () => import('@/components/ParentView/index.vue'), }
const transformComponentView = (component: string) => { return layoutComponentMap[component as keyof typeof layoutComponentMap] || asyncRouteModules[component] }
|
映射过程示例:
1 2 3 4
| transformComponentView('Layout') transformComponentView('system/user/index') transformComponentView('system/role/index')
|
🏗️ 第四步:数据格式化转换
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) }
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'), 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
| const accessRoutes = await routeStore.generateRoutes() accessRoutes.forEach((route) => { if (!isHttp(route.path)) { router.addRoute(route) } })
|
🎯 整个流程的关键优势
1. 自动化组件发现
- 无需手动维护组件映射表
- 新增页面只需放在正确目录即可
2. 类型安全
- TypeScript 类型定义确保数据结构正确
- 编译时就能发现路径错误
3. 权限控制
- 后端根据用户角色返回不同路由
- 前端只渲染有权限的页面
4. 按需加载
- 使用动态导入实现代码分割
- 只有访问时才加载对应组件
5. 灵活配置
- 路由结构可在数据库中配置
- 支持动态调整菜单结构和权限