# 动态路由流程详解 ## 📋 概述 动态路由是指根据用户权限从后端获取菜单数据,然后在前端动态生成和注册路由的过程。这样可以实现基于角色的权限控制(RBAC),不同角色的用户看到不同的菜单和页面。 --- ## 🔄 完整流程 ``` 用户登录 ↓ 路由守卫拦截 (permission.ts) ↓ 检查是否有 Token ↓ 检查用户信息是否已加载 (roles.length === 0?) ↓ 调用后端接口获取菜单数据 (/system/menu/getRouters) ↓ 后端返回菜单 JSON 数据 ↓ 前端转换路由格式 (filterAsyncRouter) ↓ 动态加载 Vue 组件 (loadView) ↓ 注册路由到 Vue Router (router.addRoute) ↓ 更新侧边栏菜单 (setSidebarRouters) ↓ 跳转到目标页面 ``` --- ## 📝 详细代码流程 ### 1️⃣ 路由守卫拦截 (`src/permission.ts`) **作用**:在每次路由跳转前进行检查,确保用户已登录且路由已加载。 ```typescript // src/permission.ts router.beforeEach(async (to, from, next) => { NProgress.start(); // 显示加载进度条 if (getToken()) { // ✅ 有 Token,已登录 if (to.path === '/login') { // 已登录用户访问登录页,重定向到首页 next({ path: '/' }); } else if (isWhiteList(to.path)) { // 白名单路径,直接放行 next(); } else { // 🔑 关键:检查用户信息是否已加载 if (useUserStore().roles.length === 0) { // 用户信息未加载,需要获取用户信息和路由 isRelogin.show = true; // 1. 获取用户信息 const [err] = await tos(useUserStore().getInfo()); if (err) { // 获取失败,退出登录 await useUserStore().logout(); next({ path: '/' }); } else { isRelogin.show = false; // 2. 🚀 生成动态路由(核心步骤) const accessRoutes = await usePermissionStore().generateRoutes(); // 3. 将路由添加到 Vue Router accessRoutes.forEach((route) => { if (!isHttp(route.path)) { router.addRoute(route); // 动态注册路由 } }); // 4. 重新跳转到目标页面(确保路由已注册) next({ path: to.path, replace: true, params: to.params, query: to.query, hash: to.hash, name: to.name as string }); } } else { // 用户信息已加载,直接放行 next(); } } } else { // ❌ 没有 Token,未登录 if (isWhiteList(to.path)) { next(); // 白名单路径 } else { // 重定向到登录页 const redirect = encodeURIComponent(to.fullPath || '/'); next(`/login?redirect=${redirect}`); } } }); ``` --- ### 2️⃣ 调用后端接口获取菜单 (`src/api/menu.ts`) **作用**:向后端请求当前用户有权限的菜单数据。 ```typescript // src/api/menu.ts export function getRouters(): AxiosPromise { return request({ url: '/system/menu/getRouters', // 后端接口地址 method: 'get' }); } ``` **后端返回的数据格式示例**: ```json [ { "name": "System", "path": "/system", "component": "Layout", "redirect": "/system/user", "alwaysShow": true, "meta": { "title": "系统管理", "icon": "system" }, "children": [ { "name": "User", "path": "user", "component": "system/user/index", "meta": { "title": "用户管理", "icon": "user" } }, { "name": "Role", "path": "role", "component": "system/role/index", "meta": { "title": "角色管理", "icon": "peoples" } } ] }, { "name": "Platform", "path": "/platform", "component": "Layout", "redirect": "/platform/carousel", "alwaysShow": true, "meta": { "title": "平台装修", "icon": "system" }, "children": [ { "name": "Carousel", "path": "carousel", "component": "platform/decoration/carousel/index", "meta": { "title": "轮播广告", "icon": "component" } } ] } ] ``` --- ### 3️⃣ 生成动态路由 (`src/store/modules/permission.ts`) **作用**:将后端返回的菜单 JSON 转换为 Vue Router 可用的路由对象。 ```typescript // src/store/modules/permission.ts const generateRoutes = async (): Promise => { // 1. 📡 调用后端接口获取菜单数据 const res = await getRouters(); const { data } = res; // 2. 📋 深拷贝数据(用于不同用途) const sdata = JSON.parse(JSON.stringify(data)); // 侧边栏路由 const rdata = JSON.parse(JSON.stringify(data)); // 重写路由(扁平化) const defaultData = JSON.parse(JSON.stringify(data)); // 默认路由 // 3. 🔄 转换路由格式 const sidebarRoutes = filterAsyncRouter(sdata); // 侧边栏菜单 const rewriteRoutes = filterAsyncRouter(rdata, undefined, true); // 扁平化路由 const defaultRoutes = filterAsyncRouter(defaultData); // 默认路由 // 4. 🔐 处理动态路由权限(如果有) const asyncRoutes = filterDynamicRoutes(dynamicRoutes); asyncRoutes.forEach((route) => { router.addRoute(route); }); // 5. 💾 保存路由到 Store setRoutes(rewriteRoutes); // 保存所有路由 setSidebarRouters(constantRoutes.concat(sidebarRoutes)); // 侧边栏路由 setDefaultRoutes(sidebarRoutes); // 默认路由 setTopbarRoutes(defaultRoutes); // 顶部栏路由 // 6. ✅ 检查路由名称是否重复 duplicateRouteChecker(asyncRoutes, sidebarRoutes); return rewriteRoutes; }; ``` --- ### 4️⃣ 转换路由格式 (`filterAsyncRouter`) **作用**:将后端返回的路由字符串转换为 Vue Router 组件对象。 ```typescript // src/store/modules/permission.ts const filterAsyncRouter = ( asyncRouterMap: RouteRecordRaw[], lastRouter?: RouteRecordRaw, type = false ): RouteRecordRaw[] => { return asyncRouterMap.filter((route) => { // 1. 🔄 如果是重写路由,扁平化子路由 if (type && route.children) { route.children = filterChildren(route.children, undefined); } // 2. 🎨 处理特殊组件 if (route.component?.toString() === 'Layout') { // Layout 组件:主布局容器 route.component = Layout; } else if (route.component?.toString() === 'ParentView') { // ParentView 组件:父级视图容器(用于嵌套路由) route.component = ParentView; } else if (route.component?.toString() === 'InnerLink') { // InnerLink 组件:内部链接 route.component = InnerLink; } else { // 3. 📦 动态加载 Vue 组件 // 例如:'system/user/index' → 加载 '@/views/system/user/index.vue' route.component = loadView(route.component, route.name as string); } // 4. 🔁 递归处理子路由 if (route.children != null && route.children && route.children.length) { route.children = filterAsyncRouter(route.children, route, type); } else { // 没有子路由,删除空属性 delete route.children; delete route.redirect; } return true; }); }; ``` **转换示例**: **转换前(后端返回)**: ```json { "name": "User", "path": "user", "component": "system/user/index", "meta": { "title": "用户管理", "icon": "user" } } ``` **转换后(Vue Router 格式)**: ```typescript { name: "User", path: "user", component: () => import('@/views/system/user/index.vue'), // 动态导入组件 meta: { title: "用户管理", icon: "user" } } ``` --- ### 5️⃣ 动态加载 Vue 组件 (`loadView`) **作用**:根据组件路径字符串,动态加载对应的 Vue 组件文件。 ```typescript // src/store/modules/permission.ts // 匹配 views 目录下所有的 .vue 文件 const modules = import.meta.glob('./../../views/**/*.vue'); export const loadView = (view: any, name: string) => { let res; // 遍历所有已匹配的组件文件 for (const path in modules) { // 例如:path = './../../views/system/user/index.vue' const viewsIndex = path.indexOf('/views/'); let dir = path.substring(viewsIndex + 7); // 'system/user/index.vue' dir = dir.substring(0, dir.lastIndexOf('.vue')); // 'system/user/index' // 匹配组件路径 if (dir === view) { // 创建自定义名称组件 res = createCustomNameComponent(modules[path], { name }); return res; } } return res; }; ``` **工作原理**: 1. `import.meta.glob()` 在构建时扫描所有 `.vue` 文件,生成一个映射对象 2. 根据后端返回的组件路径(如 `'system/user/index'`),在映射中查找对应的文件 3. 返回一个动态导入函数,Vue Router 会在需要时加载该组件 **modules 对象示例**: ```typescript { './../../views/system/user/index.vue': () => import('./../../views/system/user/index.vue'), './../../views/system/role/index.vue': () => import('./../../views/system/role/index.vue'), './../../views/platform/decoration/carousel/index.vue': () => import('./../../views/platform/decoration/carousel/index.vue'), // ... 更多组件 } ``` --- ### 6️⃣ 扁平化子路由 (`filterChildren`) **作用**:将嵌套的子路由扁平化,用于路由重写。 ```typescript // src/store/modules/permission.ts const filterChildren = ( childrenMap: RouteRecordRaw[], lastRouter?: RouteRecordRaw ): RouteRecordRaw[] => { let children: RouteRecordRaw[] = []; childrenMap.forEach((el) => { // 拼接完整路径 // 例如:父路由 '/platform' + 子路由 'carousel' = '/platform/carousel' el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path; // 如果子路由还有子路由,且使用 ParentView,继续递归扁平化 if (el.children && el.children.length && el.component?.toString() === 'ParentView') { children = children.concat(filterChildren(el.children, el)); } else { children.push(el); } }); return children; }; ``` **示例**: **扁平化前**: ```typescript { path: '/platform', children: [ { path: 'decoration', children: [ { path: 'carousel' }, { path: 'solution' } ] } ] } ``` **扁平化后**: ```typescript [ { path: '/platform/decoration/carousel' }, { path: '/platform/decoration/solution' } ] ``` --- ### 7️⃣ 注册路由到 Vue Router **作用**:将转换后的路由添加到 Vue Router 实例中。 ```typescript // src/permission.ts (在路由守卫中) const accessRoutes = await usePermissionStore().generateRoutes(); // 遍历所有路由,添加到 Vue Router accessRoutes.forEach((route) => { if (!isHttp(route.path)) { router.addRoute(route); // 动态注册路由 } }); ``` **`router.addRoute()` 的作用**: - 将路由添加到路由表中 - 支持嵌套路由 - 支持命名路由 - 支持路由元信息(meta) --- ### 8️⃣ 更新侧边栏菜单 **作用**:将生成的路由保存到 Store,供侧边栏组件使用。 ```typescript // src/store/modules/permission.ts setSidebarRouters(constantRoutes.concat(sidebarRoutes)); ``` **侧边栏组件使用**: ```vue ``` --- ## 🎯 完整示例:平台装修菜单 ### 后端返回的数据: ```json { "name": "Platform", "path": "/platform", "component": "Layout", "redirect": "/platform/carousel", "alwaysShow": true, "meta": { "title": "平台装修", "icon": "system" }, "children": [ { "name": "Carousel", "path": "carousel", "component": "platform/decoration/carousel/index", "meta": { "title": "轮播广告", "icon": "component" } }, { "name": "Solution", "path": "solution", "component": "platform/decoration/solution/index", "meta": { "title": "方案管理", "icon": "documentation" } } ] } ``` ### 转换后的路由对象: ```typescript { name: "Platform", path: "/platform", component: Layout, // 已转换为组件对象 redirect: "/platform/carousel", alwaysShow: true, meta: { title: "平台装修", icon: "system" }, children: [ { name: "Carousel", path: "carousel", component: () => import('@/views/platform/decoration/carousel/index.vue'), meta: { title: "轮播广告", icon: "component" } }, { name: "Solution", path: "solution", component: () => import('@/views/platform/decoration/solution/index.vue'), meta: { title: "方案管理", icon: "documentation" } } ] } ``` ### 扁平化后的路由(用于路由重写): ```typescript [ { name: "Carousel", path: "/platform/carousel", component: () => import('@/views/platform/decoration/carousel/index.vue'), meta: { title: "轮播广告", icon: "component" } }, { name: "Solution", path: "/platform/solution", component: () => import('@/views/platform/decoration/solution/index.vue'), meta: { title: "方案管理", icon: "documentation" } } ] ``` --- ## 🔍 关键点总结 1. **路由守卫**:在每次路由跳转前检查并加载动态路由 2. **后端接口**:获取用户有权限的菜单数据 3. **路由转换**:将 JSON 数据转换为 Vue Router 格式 4. **组件加载**:动态导入 Vue 组件文件 5. **路由注册**:使用 `router.addRoute()` 添加到路由表 6. **菜单更新**:更新侧边栏菜单显示 --- ## ⚠️ 注意事项 1. **路由名称不能重复**:`duplicateRouteChecker` 会检查并报错 2. **组件路径必须正确**:后端返回的 `component` 路径必须对应 `views` 目录下的文件 3. **首次加载**:只在用户信息未加载时(`roles.length === 0`)才生成路由 4. **路由重定向**:生成路由后需要重新跳转,确保路由已注册 5. **平台标识**:后端会根据 `X-Platform-Code` 头过滤菜单 --- ## 🚀 优势 1. **权限控制**:不同角色看到不同的菜单和页面 2. **灵活配置**:菜单配置在数据库中,无需修改代码 3. **代码分割**:使用动态导入,实现按需加载 4. **易于维护**:菜单结构清晰,易于管理