Skip to content

Latest commit

 

History

History
631 lines (534 loc) · 14.3 KB

File metadata and controls

631 lines (534 loc) · 14.3 KB

dynamicRoutes.js 动态路由工具文档

概述

dynamicRoutes.js是动态路由生成工具,根据用户权限和菜单配置动态生成路由,实现基于权限的路由控制。

文件位置: src/utils/dynamicRoutes.js

主要功能

1. 路由生成功能

  • 根据菜单数据生成路由
  • 权限验证和过滤
  • 组件懒加载
  • 路由元信息配置
  • 嵌套路由处理
  • 重定向路由生成

2. 核心方法

generateRoutes

/**
 * 根据菜单数据生成动态路由
 * @param {Array} menuList - 菜单列表
 * @param {Array} userPermissions - 用户权限列表
 * @returns {Array} 生成的路由配置
 */
export function generateRoutes(menuList, userPermissions = []) {
  if (!Array.isArray(menuList) || menuList.length === 0) {
    return []
  }
  
  const routes = []
  
  menuList.forEach(menu => {
    // 检查权限
    if (!hasPermission(menu, userPermissions)) {
      return
    }
    
    const route = createRoute(menu)
    
    // 处理子菜单
    if (menu.children && menu.children.length > 0) {
      route.children = generateRoutes(menu.children, userPermissions)
    }
    
    routes.push(route)
  })
  
  return routes
}

createRoute

/**
 * 创建单个路由配置
 * @param {Object} menu - 菜单项
 * @returns {Object} 路由配置对象
 */
function createRoute(menu) {
  const route = {
    path: menu.path,
    name: menu.name || generateRouteName(menu.path),
    meta: {
      title: menu.title,
      icon: menu.icon,
      requiresAuth: menu.requiresAuth !== false,
      permissions: menu.permissions || [],
      roles: menu.roles || [],
      hidden: menu.hidden || false,
      keepAlive: menu.keepAlive || false,
      breadcrumb: menu.breadcrumb !== false,
      activeMenu: menu.activeMenu,
      noCache: menu.noCache || false,
      affix: menu.affix || false
    }
  }
  
  // 设置组件
  if (menu.component) {
    route.component = loadComponent(menu.component)
  } else if (menu.children && menu.children.length > 0) {
    // 如果有子路由但没有组件,使用布局组件
    route.component = () => import('@/components/Layout/LayoutContainer.vue')
  }
  
  // 设置重定向
  if (menu.redirect) {
    route.redirect = menu.redirect
  } else if (menu.children && menu.children.length > 0) {
    // 自动重定向到第一个子路由
    const firstChild = menu.children.find(child => !child.hidden)
    if (firstChild) {
      route.redirect = firstChild.path
    }
  }
  
  return route
}

loadComponent

/**
 * 动态加载组件
 * @param {String} componentPath - 组件路径
 * @returns {Function} 组件加载函数
 */
function loadComponent(componentPath) {
  // 处理不同的组件路径格式
  if (componentPath.startsWith('/')) {
    componentPath = componentPath.slice(1)
  }
  
  // 添加文件扩展名
  if (!componentPath.endsWith('.vue')) {
    componentPath += '.vue'
  }
  
  // 使用动态导入
  return () => import(`@/views/${componentPath}`)
    .catch(error => {
      console.error(`Failed to load component: ${componentPath}`, error)
      // 返回404组件作为降级
      return import('@/views/NotFound.vue')
    })
}

hasPermission

/**
 * 检查用户是否有访问菜单的权限
 * @param {Object} menu - 菜单项
 * @param {Array} userPermissions - 用户权限列表
 * @returns {Boolean} 是否有权限
 */
function hasPermission(menu, userPermissions) {
  // 如果菜单没有设置权限要求,默认允许访问
  if (!menu.permissions || menu.permissions.length === 0) {
    return true
  }
  
  // 检查用户是否有任一所需权限
  return menu.permissions.some(permission => 
    userPermissions.includes(permission)
  )
}

generateRouteName

/**
 * 根据路径生成路由名称
 * @param {String} path - 路由路径
 * @returns {String} 路由名称
 */
function generateRouteName(path) {
  return path
    .split('/')
    .filter(segment => segment)
    .map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
    .join('')
}

3. 路由处理工具

flattenRoutes

/**
 * 扁平化路由结构
 * @param {Array} routes - 路由数组
 * @returns {Array} 扁平化后的路由数组
 */
export function flattenRoutes(routes) {
  const flattened = []
  
  function flatten(routeList, parentPath = '') {
    routeList.forEach(route => {
      const fullPath = parentPath + route.path
      
      flattened.push({
        ...route,
        path: fullPath,
        children: undefined
      })
      
      if (route.children && route.children.length > 0) {
        flatten(route.children, fullPath + '/')
      }
    })
  }
  
  flatten(routes)
  return flattened
}

findRouteByPath

/**
 * 根据路径查找路由
 * @param {Array} routes - 路由数组
 * @param {String} path - 路径
 * @returns {Object|null} 找到的路由对象
 */
export function findRouteByPath(routes, path) {
  for (const route of routes) {
    if (route.path === path) {
      return route
    }
    
    if (route.children && route.children.length > 0) {
      const found = findRouteByPath(route.children, path)
      if (found) {
        return found
      }
    }
  }
  
  return null
}

getRoutePermissions

/**
 * 获取路由所需的权限
 * @param {Object} route - 路由对象
 * @returns {Array} 权限数组
 */
export function getRoutePermissions(route) {
  const permissions = []
  
  if (route.meta && route.meta.permissions) {
    permissions.push(...route.meta.permissions)
  }
  
  return [...new Set(permissions)]
}

4. 路由守卫工具

createRouteGuard

/**
 * 创建路由守卫
 * @param {Object} options - 守卫配置
 * @returns {Function} 路由守卫函数
 */
export function createRouteGuard(options = {}) {
  const {
    requiresAuth = true,
    checkPermissions = true,
    redirectTo = '/login'
  } = options
  
  return (to, from, next) => {
    // 检查认证
    if (requiresAuth && to.meta.requiresAuth) {
      const token = localStorage.getItem('token')
      if (!token) {
        next(redirectTo)
        return
      }
    }
    
    // 检查权限
    if (checkPermissions && to.meta.permissions) {
      const userPermissions = getUserPermissions()
      const hasRequiredPermission = to.meta.permissions.some(permission =>
        userPermissions.includes(permission)
      )
      
      if (!hasRequiredPermission) {
        next('/403')
        return
      }
    }
    
    next()
  }
}

getUserPermissions

/**
 * 获取当前用户权限
 * @returns {Array} 用户权限数组
 */
function getUserPermissions() {
  try {
    const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
    return userInfo.permissions || []
  } catch (error) {
    console.error('Failed to get user permissions:', error)
    return []
  }
}

5. 路由缓存管理

RouteCache

/**
 * 路由缓存管理类
 */
export class RouteCache {
  constructor() {
    this.cache = new Map()
    this.maxSize = 50
  }
  
  /**
   * 设置路由缓存
   * @param {String} key - 缓存键
   * @param {Object} route - 路由对象
   */
  set(key, route) {
    if (this.cache.size >= this.maxSize) {
      // 删除最旧的缓存
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    
    this.cache.set(key, {
      route,
      timestamp: Date.now()
    })
  }
  
  /**
   * 获取路由缓存
   * @param {String} key - 缓存键
   * @returns {Object|null} 缓存的路由对象
   */
  get(key) {
    const cached = this.cache.get(key)
    if (!cached) {
      return null
    }
    
    // 检查缓存是否过期(1小时)
    const isExpired = Date.now() - cached.timestamp > 60 * 60 * 1000
    if (isExpired) {
      this.cache.delete(key)
      return null
    }
    
    return cached.route
  }
  
  /**
   * 清除所有缓存
   */
  clear() {
    this.cache.clear()
  }
  
  /**
   * 删除指定缓存
   * @param {String} key - 缓存键
   */
  delete(key) {
    this.cache.delete(key)
  }
}

// 创建全局路由缓存实例
export const routeCache = new RouteCache()

6. 路由工具函数

normalizeRoutes

/**
 * 标准化路由配置
 * @param {Array} routes - 原始路由数组
 * @returns {Array} 标准化后的路由数组
 */
export function normalizeRoutes(routes) {
  return routes.map(route => {
    const normalized = { ...route }
    
    // 确保路径以/开头
    if (normalized.path && !normalized.path.startsWith('/')) {
      normalized.path = '/' + normalized.path
    }
    
    // 确保meta对象存在
    if (!normalized.meta) {
      normalized.meta = {}
    }
    
    // 设置默认值
    normalized.meta.requiresAuth = normalized.meta.requiresAuth !== false
    normalized.meta.hidden = normalized.meta.hidden || false
    normalized.meta.keepAlive = normalized.meta.keepAlive || false
    
    // 处理子路由
    if (normalized.children && normalized.children.length > 0) {
      normalized.children = normalizeRoutes(normalized.children)
    }
    
    return normalized
  })
}

validateRoutes

/**
 * 验证路由配置
 * @param {Array} routes - 路由数组
 * @returns {Object} 验证结果
 */
export function validateRoutes(routes) {
  const errors = []
  const warnings = []
  
  function validate(routeList, parentPath = '') {
    routeList.forEach((route, index) => {
      const routePath = `${parentPath}[${index}]`
      
      // 检查必需字段
      if (!route.path) {
        errors.push(`${routePath}: Missing required field 'path'`)
      }
      
      if (!route.name && !route.component) {
        warnings.push(`${routePath}: Route should have either 'name' or 'component'`)
      }
      
      // 检查路径格式
      if (route.path && !route.path.startsWith('/') && parentPath === '') {
        warnings.push(`${routePath}: Root route path should start with '/'`)
      }
      
      // 检查组件
      if (route.component && typeof route.component !== 'function') {
        errors.push(`${routePath}: Component should be a function`)
      }
      
      // 递归检查子路由
      if (route.children && route.children.length > 0) {
        validate(route.children, `${routePath}.children`)
      }
    })
  }
  
  validate(routes)
  
  return {
    isValid: errors.length === 0,
    errors,
    warnings
  }
}

使用示例

1. 基础使用

import { generateRoutes } from '@/utils/dynamicRoutes'

// 菜单数据
const menuList = [
  {
    path: '/dashboard',
    title: '仪表盘',
    icon: 'Dashboard',
    component: 'Dashboard',
    permissions: ['dashboard:view']
  },
  {
    path: '/user',
    title: '用户管理',
    icon: 'User',
    permissions: ['user:view'],
    children: [
      {
        path: '/user/list',
        title: '用户列表',
        component: 'user/index',
        permissions: ['user:list']
      }
    ]
  }
]

// 用户权限
const userPermissions = ['dashboard:view', 'user:view', 'user:list']

// 生成路由
const routes = generateRoutes(menuList, userPermissions)

2. 在Vue Router中使用

import { createRouter, createWebHistory } from 'vue-router'
import { generateRoutes, createRouteGuard } from '@/utils/dynamicRoutes'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // 静态路由
    {
      path: '/login',
      component: () => import('@/views/Login.vue')
    }
  ]
})

// 动态添加路由
export async function setupDynamicRoutes() {
  try {
    // 获取菜单数据
    const menuList = await getMenuList()
    const userPermissions = await getUserPermissions()
    
    // 生成动态路由
    const dynamicRoutes = generateRoutes(menuList, userPermissions)
    
    // 添加到路由器
    dynamicRoutes.forEach(route => {
      router.addRoute(route)
    })
    
    // 添加路由守卫
    const routeGuard = createRouteGuard({
      requiresAuth: true,
      checkPermissions: true,
      redirectTo: '/login'
    })
    
    router.beforeEach(routeGuard)
    
  } catch (error) {
    console.error('Failed to setup dynamic routes:', error)
  }
}

3. 路由缓存使用

import { routeCache } from '@/utils/dynamicRoutes'

// 缓存路由
routeCache.set('user-routes', userRoutes)

// 获取缓存
const cachedRoutes = routeCache.get('user-routes')

// 清除缓存
routeCache.clear()

4. 路由验证

import { validateRoutes, normalizeRoutes } from '@/utils/dynamicRoutes'

// 验证路由配置
const validation = validateRoutes(routes)
if (!validation.isValid) {
  console.error('Route validation errors:', validation.errors)
}

// 标准化路由
const normalizedRoutes = normalizeRoutes(routes)

配置选项

1. 菜单数据格式

const menuItem = {
  path: '/user',              // 路由路径
  name: 'User',               // 路由名称(可选)
  title: '用户管理',           // 菜单标题
  icon: 'User',               // 菜单图标
  component: 'user/index',    // 组件路径
  permissions: ['user:view'], // 所需权限
  roles: ['admin'],           // 所需角色
  hidden: false,              // 是否隐藏
  keepAlive: true,            // 是否缓存
  requiresAuth: true,         // 是否需要认证
  breadcrumb: true,           // 是否显示面包屑
  activeMenu: '/user/list',   // 激活的菜单项
  redirect: '/user/list',     // 重定向路径
  children: []                // 子菜单
}

2. 路由守卫配置

const guardOptions = {
  requiresAuth: true,         // 是否检查认证
  checkPermissions: true,     // 是否检查权限
  redirectTo: '/login',       // 重定向路径
  whiteList: ['/login'],      // 白名单路径
  beforeEnter: (to, from) => {}, // 自定义前置守卫
  afterEnter: (to, from) => {}   // 自定义后置守卫
}

注意事项

  1. 组件路径: 确保组件路径正确,支持相对路径和绝对路径
  2. 权限检查: 权限检查基于数组包含关系,支持多权限验证
  3. 路由缓存: 合理使用路由缓存,避免内存泄漏
  4. 错误处理: 组件加载失败时会降级到404页面
  5. 性能优化: 大量路由时考虑懒加载和分批处理

相关文档


最后更新时间:2025-09-19