# 动态路由流程详解

## 📋 概述

动态路由是指根据用户权限从后端获取菜单数据，然后在前端动态生成和注册路由的过程。这样可以实现基于角色的权限控制（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<RouteRecordRaw[]> {
  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<RouteRecordRaw[]> => {
  // 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
<!-- src/layout/components/Sidebar/index.vue -->
<template>
  <el-menu :default-active="activeMenu">
    <sidebar-item 
      v-for="route in sidebarRouters" 
      :key="route.path" 
      :item="route"
    />
  </el-menu>
</template>

<script setup>
import { usePermissionStore } from '@/store/modules/permission';

const permissionStore = usePermissionStore();
const sidebarRouters = computed(() => permissionStore.getSidebarRoutes());
</script>
```

---

## 🎯 完整示例：平台装修菜单

### 后端返回的数据：
```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. **易于维护**：菜单结构清晰，易于管理

