动态路由代码示例.md 8.8 KB

动态路由代码示例

📦 简化版代码示例

1. 路由守卫(简化版)

// src/permission.ts (简化版)

router.beforeEach(async (to, from, next) => {
  // 1. 检查是否已登录
  if (getToken()) {
    // 2. 检查用户信息是否已加载
    if (useUserStore().roles.length === 0) {
      // 3. 获取用户信息
      await useUserStore().getInfo();
      
      // 4. 生成动态路由
      const routes = await usePermissionStore().generateRoutes();
      
      // 5. 注册路由
      routes.forEach(route => {
        router.addRoute(route);
      });
      
      // 6. 重新跳转
      next({ ...to, replace: true });
    } else {
      next();
    }
  } else {
    next('/login');
  }
});

2. 生成路由(简化版)

// src/store/modules/permission.ts (简化版)

const generateRoutes = async () => {
  // 步骤1: 调用后端接口
  const res = await getRouters();
  const menuData = res.data;
  
  // 步骤2: 转换路由格式
  const routes = transformRoutes(menuData);
  
  // 步骤3: 保存到 Store
  setSidebarRouters(routes);
  
  return routes;
};

// 转换函数(简化版)
const transformRoutes = (menuData) => {
  return menuData.map(menu => {
    const route = {
      name: menu.name,
      path: menu.path,
      component: loadComponent(menu.component), // 动态加载组件
      meta: menu.meta,
      children: menu.children ? transformRoutes(menu.children) : undefined
    };
    
    // 处理特殊组件
    if (menu.component === 'Layout') {
      route.component = Layout;
    }
    
    return route;
  });
};

// 加载组件(简化版)
const loadComponent = (componentPath) => {
  // componentPath: 'system/user/index'
  // 返回: () => import('@/views/system/user/index.vue')
  return () => import(`@/views/${componentPath}.vue`);
};

3. 后端返回数据示例

// GET /system/menu/getRouters 返回的数据

[
  {
    "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": "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"
        }
      }
    ]
  }
]

4. 转换后的路由对象示例

// 转换后的路由对象(Vue Router 格式)

[
  {
    name: "System",
    path: "/system",
    component: Layout, // 已转换为组件对象
    redirect: "/system/user",
    alwaysShow: true,
    meta: {
      title: "系统管理",
      icon: "system"
    },
    children: [
      {
        name: "User",
        path: "user",
        component: () => import('@/views/system/user/index.vue'), // 动态导入
        meta: {
          title: "用户管理",
          icon: "user"
        }
      }
    ]
  },
  {
    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"
        }
      }
    ]
  }
]

5. 注册路由示例

// 在路由守卫中注册路由

const routes = await generateRoutes();

routes.forEach(route => {
  // 将路由添加到 Vue Router
  router.addRoute(route);
  
  // 现在可以通过以下方式访问:
  // router.push('/system/user')
  // router.push('/platform/carousel')
});

6. 侧边栏使用路由示例

<!-- src/layout/components/Sidebar/index.vue -->

<template>
  <el-menu :default-active="activeMenu">
    <template v-for="route in sidebarRouters" :key="route.path">
      <!-- 有子菜单 -->
      <el-sub-menu v-if="route.children" :index="route.path">
        <template #title>
          <el-icon><component :is="route.meta.icon" /></el-icon>
          <span>{{ route.meta.title }}</span>
        </template>
        <el-menu-item 
          v-for="child in route.children" 
          :key="child.path"
          :index="child.path"
          @click="handleMenuClick(child)"
        >
          {{ child.meta.title }}
        </el-menu-item>
      </el-sub-menu>
      
      <!-- 无子菜单 -->
      <el-menu-item v-else :index="route.path" @click="handleMenuClick(route)">
        <el-icon><component :is="route.meta.icon" /></el-icon>
        <span>{{ route.meta.title }}</span>
      </el-menu-item>
    </template>
  </el-menu>
</template>

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

const permissionStore = usePermissionStore();
const router = useRouter();

// 获取侧边栏路由
const sidebarRouters = computed(() => {
  return permissionStore.getSidebarRoutes();
});

// 菜单点击事件
const handleMenuClick = (route) => {
  router.push(route.path);
};
</script>

🔄 完整流程示例

场景:用户登录后访问 /platform/carousel

// 步骤1: 用户访问 /platform/carousel
router.push('/platform/carousel');

// 步骤2: 路由守卫拦截
router.beforeEach(async (to, from, next) => {
  // to.path = '/platform/carousel'
  
  // 步骤3: 检查 Token
  if (getToken()) {
    // 步骤4: 检查用户信息
    if (useUserStore().roles.length === 0) {
      // 步骤5: 获取用户信息
      await useUserStore().getInfo();
      
      // 步骤6: 调用后端接口
      const res = await getRouters();
      // res.data = [{ name: "Platform", path: "/platform", ... }]
      
      // 步骤7: 转换路由
      const routes = await generateRoutes();
      // routes = [{ name: "Platform", component: Layout, ... }]
      
      // 步骤8: 注册路由
      routes.forEach(route => {
        router.addRoute(route);
      });
      
      // 步骤9: 重新跳转
      next({ path: '/platform/carousel', replace: true });
    } else {
      // 用户信息已加载,直接放行
      next();
    }
  }
});

// 步骤10: 路由匹配成功,显示页面
// <router-view> 渲染 Carousel 组件

📊 数据流转图

后端数据库 (sys_menu)
    ↓
后端接口 (/system/menu/getRouters)
    ↓
前端 API (getRouters())
    ↓
路由转换 (filterAsyncRouter)
    ↓
组件加载 (loadView)
    ↓
路由注册 (router.addRoute)
    ↓
侧边栏显示 (sidebarRouters)
    ↓
页面渲染 (<router-view>)

🎯 关键函数说明

generateRoutes()

  • 作用:生成动态路由的主函数
  • 输入:无(内部调用后端接口)
  • 输出:路由对象数组
  • 流程:获取数据 → 转换格式 → 保存到 Store → 返回路由

filterAsyncRouter()

  • 作用:将后端 JSON 转换为 Vue Router 格式
  • 输入:后端返回的路由 JSON 数组
  • 输出:Vue Router 路由对象数组
  • 关键:处理组件加载、递归处理子路由

loadView()

  • 作用:根据路径字符串动态加载 Vue 组件
  • 输入:组件路径字符串(如 'system/user/index'
  • 输出:组件导入函数(如 () => import('@/views/system/user/index.vue')
  • 原理:使用 import.meta.glob() 预扫描所有组件文件

router.addRoute()

  • 作用:将路由添加到 Vue Router 实例
  • 输入:路由对象
  • 输出:无
  • 效果:路由立即可用,可以通过 router.push() 访问

💡 常见问题

Q1: 为什么需要重新跳转?

A: 因为路由是异步注册的,需要确保路由已注册后再跳转,否则会 404。

Q2: 组件路径如何匹配?

A: 后端返回的 component 路径(如 'system/user/index')必须对应 views 目录下的文件路径(views/system/user/index.vue)。

Q3: 路由名称重复会怎样?

A: duplicateRouteChecker 会检查并报错,可能导致路由冲突和 404。

Q4: 如何实现权限控制?

A: 后端根据用户角色和权限过滤菜单数据,前端只显示后端返回的菜单。

Q5: 静态路由和动态路由的区别?

A:

  • 静态路由:写在 router/index.ts 中,所有用户都能访问
  • 动态路由:从后端获取,根据用户权限动态生成