Back to blog
May 24, 2024
11 min read

路由架构(二)用户权限路由配置

生成基于用户权限的路由配置。从用户的权限信息中动态构建应用的路由结构,确保用户只能访问他们有权限的页面。

use-permission-routes.tsx

当前函数可能是当前前端应用中最复杂的部分

const entryPath = '/src/pages';
const pages = import.meta.glob('/src/pages/**/*.tsx');
export const pagesSelect = Object.entries(pages).map(([path]) => {
  const pagePath = path.replace(entryPath, '');
  return {
    label: pagePath,
    value: pagePath,
  };
});

使用 import.meta.glob 动态匹配 src/pages 目录下的所有 .tsx 文件,并返回一个包含这些文件路径和导入函数的对象。返回的 pages,内容大概如下

{
  '/src/pages/Home.tsx': () => import('/src/pages/Home.tsx'),
  '/src/pages/About.tsx': () => import('/src/pages/About.tsx'),
  // 其他文件...
}

pagesSelect 的功能是生成一个页面路径的选择列表,这在一些场景中非常有用。例如,你可以在一个下拉菜单中显示所有可用的页面,供用户选择。

  1. Object.entries(pages):将 pages 对象转换为一个数组,其中每个元素是一个 [path, importFunction] 的数组。
  2. 遍历每个 [path, importFunction] 数组,提取文件路径 path。使用 path.replace(entryPath, ”) 去掉文件路径中的 entryPath 部分,获取相对路径 pagePath。创建一个对象 { label: pagePath, value: pagePath },其中 label 和 value 都是相对路径。
    1. entryPath内容是:/src/pages
    2. 去除 entryPath 的目的是,使路径变得更加简洁和易读
    3. /src/pages/components/animate/index.tsx -> /components/animate/index.tsx
    4. /src/pages/components/animate/index.tsx -> /components/animate/index.tsx
    5. /src/pages/dashboard/analysis/analysis-card.tsx -> /dashboard/analysis/analysis-card.tsx
    6. /src/pages/dashboard/analysis/analysis-news.tsx -> /dashboard/analysis/analysis-news.tsx
    7. /src/pages/management/blog/index.tsx -> /management/blog/index.tsx
    8. /src/pages/management/system/organization/index.tsx -> /management/system/organization/index.tsx
  3. 最终生成一个包含所有页面相对路径的数组,每个元素是一个 { label, value } 对象。
// path路径的内容
/src/pages/components/animate/index.tsx
/src/pages/dashboard/analysis/analysis-card.tsx
/src/pages/dashboard/analysis/analysis-news.tsx
/src/pages/management/blog/index.tsx
/src/pages/management/system/organization/index.tsx

最后的导出的结果

[
  {label: '/components/chart/view/chart-donut.tsx', value: '/components/chart/view/chart-donut.tsx'},
  {label: '/components/chart/view/chart-line.tsx', value: '/components/chart/view/chart-line.tsx'},
  {label: '/components/chart/view/chart-mixed.tsx', value: '/components/chart/view/chart-mixed.tsx'},
  {label: '/dashboard/workbench/index.tsx', value: '/dashboard/workbench/index.tsx'},
  {label: '/dashboard/workbench/new-invoice.tsx', value: '/dashboard/workbench/new-invoice.tsx'},
  {label: '/sys/error/Page403.tsx', value: '/sys/error/Page403.tsx'},
  {label: '/sys/error/Page404.tsx', value: '/sys/error/Page404.tsx'},
  {label: '/sys/error/Page500.tsx', value: '/sys/error/Page500.tsx'},
]

usePermissionRoutes

在一个基于权限的应用中,不同用户可能具有不同的访问权限。因此,路由配置也需要根据用户的权限动态生成,以确保用户只能访问他们有权限的页面。 这个功能通过自定义 Hook usePermissionRoutes 来实现

// 将用户的权限数据转化为路由对象
export function usePermissionRoutes() {
  const permissions = useUserPermission();  // 获取当前用户权限信息

  return useMemo(() => {
    const flattenedPermissions = flattenTrees(permissions!);  // 将权限树结构展平成一维数组
    const permissionRoutes = transformPermissionToMenuRoutes(
      permissions || [],
      flattenedPermissions,
    );  // 将权限转换为路由配置
    
    return [...permissionRoutes];  // 返回路由配置数组
  }, [permissions]);  // 当权限变化时重新计算
}
  1. useUserPermission 是一个自定义 Hook,用于从用户状态或存储中获取当前用户的权限信息。
  2. 返回的 permissions 通常是一个权限树,包含用户对各个页面或功能的访问权限。
  3. flattenTrees 是一个工具函数,将权限树结构展平成一个数组。这样做的目的是为了方便在后续的转换过程中快速查找和使用权限节点。
  4. transformPermissionToMenuRoutes 是一个函数,用于将权限数据转换为路由配置。
  5. 使用 useMemo 缓存计算结果,以避免每次渲染时都重新计算路由对象,提升性能。

transformPermissionToMenuRoutes

将权限数组转化为路由配置:主要功能是将权限数组转换为路由配置对象(AppRouteObject[])。它根据每个权限对象的属性生成对应的路由对象,并且递归处理子权限。

function transformPermissionToMenuRoutes(
  permissions: Permission[],
  flattenedPermissions: Permission[],
) {
  return permissions.map((permission) => {
    const {
      route,
      type,
      label,
      icon,
      order,
      hide,
      hideTab,
      status,
      frameSrc,
      newFeature,
      component,
      parentId,
      children = [],
    } = permission;

    const appRoute: AppRouteObject = {
      path: route,
      meta: {
        label,
        key: getCompleteRoute(permission, flattenedPermissions),
        hideMenu: !!hide,
        hideTab,
        disabled: status === BasicStatus.DISABLE,
      },
    };

    if (order) appRoute.order = order;
    if (icon) appRoute.meta!.icon = icon;
    if (frameSrc) appRoute.meta!.frameSrc = frameSrc;

    if (newFeature) {
      appRoute.meta!.suffix = (
        <ProTag color="cyan" icon={<Iconify icon="solar:bell-bing-bold-duotone" size={14} />}>
          NEW
        </ProTag>
      );
    }

    if (type === PermissionType.CATALOGUE) {
      appRoute.meta!.hideTab = true;
      if (!parentId) {
        appRoute.element = (
          <Suspense fallback={<CircleLoading />}>
            <Outlet />
          </Suspense>
        );
      }
      appRoute.children = transformPermissionToMenuRoutes(children, flattenedPermissions);

      if (!isEmpty(children)) {
        appRoute.children.unshift({
          index: true,
          element: <Navigate to={children[0].route} replace />,
        });
      }
    } else if (type === PermissionType.MENU) {
      const Element = lazy(resolveComponent(component!) as any);
      if (frameSrc) {
        appRoute.element = <Element src={frameSrc} />;
      } else {
        appRoute.element = (
          <Suspense fallback={<CircleLoading />}>
            <Element />
          </Suspense>
        );
      }
    }

    return appRoute;
  });
}

状态管理

permissions: 权限,许可

在一个复杂的应用中,用户的权限信息通常会影响到多个方面,比如:

  1. 页面和功能的可见性。
  2. 操作的可用性。
  3. 路由的访问控制。

为了便于管理和访问这些权限信息,通常会将权限信息存储在一个全局状态(如 Zustand 状态管理库)中,并通过自定义 Hook 提供访问权限信息的方式。

getCompleteRoute

function getCompleteRoute(permission: Permission, flattenedPermissions: Permission[], route = '') {
  const currentRoute = route ? `/${permission.route}${route}` : `/${permission.route}`;

  if (permission.parentId) {
    const parentPermission = flattenedPermissions.find((p) => p.id === permission.parentId)!;
    return getCompleteRoute(parentPermission, flattenedPermissions, currentRoute);
  }

  return currentRoute;
}

主要目的是生成完整的权限路由路径。函数通过递归遍历权限节点的父节点,一层层地拼接路径,从而生成从根节点到当前权限节点的完整路径。在权限系统中,权限节点可能具有父子关系。例如,一个子菜单的路径可能依赖于父菜单的路径。为了生成正确的路由配置,需要知道每个节点的完整路径,而不仅仅是当前节点的相对路径。getCompleteRoute 函数解决了这个问题。

示例代码

const flattenedPermissions = [
  { id: 1, route: 'dashboard', parentId: null },
  { id: 2, route: 'overview', parentId: 1 },
  { id: 3, route: 'reports', parentId: 1 },
  { id: 4, route: 'details', parentId: 3 }
];

const permission = { id: 4, route: 'details', parentId: 3 };

调用 getCompleteRoute ,详细执行流程如下:

  1. 初始调用getCompleteRoute({ id: 4, route: 'details', parentId: 3 }, flattenedPermissions)
    1. 因为没有第三个参数,所以:currentRoute = '/details'
    2. 查找到当前内容有父节点 id: 3 -> { id: 3, route: 'reports', parentId: 1 }
    3. 递归调用 getCompleteRoute 传入父节点和当前路径 ‘/details’
  2. 递归调用1getCompleteRoute({ id: 3, route: 'reports', parentId: 1 }, flattenedPermissions, '/details')
    1. 因为有第三个参数,所以触发父子拼接逻辑:currentRoute = '/reports/details'
    2. 查找到当前内容有父节点 id: 1,找到 { id: 1, route: 'dashboard', parentId: null }
    3. 递归调用 getCompleteRoute 传入父节点和当前路径 ‘/reports/details’
  3. 递归调用2getCompleteRoute({ id: 1, route: 'dashboard', parentId: null }, flattenedPermissions, '/reports/details')
    1. 因为有第三个参数,所以触发父子拼接逻辑:currentRoute = '/dashboard/reports/details'
    2. 没有父节点,递归终止,返回 '/dashboard/reports/details'

处理目录类型权限

目录类型的权限节点正是指左侧菜单栏中的分组和层级节点。目录节点用于组织和包含其他权限节点,使菜单结构层次分明、便于导航。

 if (type === PermissionType.CATALOGUE) {
   appRoute.meta!.hideTab = true; // 设置隐藏标签: 目录类型的权限在标签中通常不显示,因为它只是用来组织子菜单或子目录的
  //  没有parentId,意味着这是一个顶级目录
   if (!parentId) {
    // 设置一个Outlet,方便渲染子路由(每一个包含子级的路由,都要设置一个Outlet,这是路由的设计规则,否则无法正常显示)
     appRoute.element = (
       <Suspense fallback={<CircleLoading />}>
         <Outlet />
       </Suspense>
     );
   }
  // 递归处理权限:将子权限递归地转换为子路由配置
   appRoute.children = transformPermissionToMenuRoutes(children, flattenedPermissions);

   if (!isEmpty(children)) {
    // 如果目中没有子权限,就设置一个默认路由,当访问目录路径时,自动挑战到第一个子权限对应的路由
     appRoute.children.unshift({
       index: true,
       element: <Navigate to={children[0].route} replace />,
     });
   }
 }

处理目录类型的权限:

  1. 如果权限类型是 PermissionType.CATALOGUE,则将其处理为目录路由。
    1. 这意味着这个路由项主要是为了包含其他子路由,而不是一个具体的页面组件。目录路由用于结构化地组织子路由。
  2. 顶级目录添加 Outlet 以支持嵌套路由。
    1. 如果目录项是顶级目录(没有父级目录),我们需要给它添加一个 Outlet 组件。
    2. Outlet 是 React Router 中用于渲染嵌套路由的占位符。当一个父级路由被匹配时,Outlet 将渲染其子路由的组件。
  3. 递归调用 transformPermissionToMenuRoutes 来处理子权限。
    1. 如果目录项包含子权限,我们通常希望用户访问这个目录时,默认显示第一个子权限对应的页面。
    2. 为此,我们在子路由列表的开头添加一个索引路由,使用 <Navigate> 组件将用户重定向到第一个子权限的路径。
  4. 如果有子权限,添加一个默认跳转到第一个子路由的索引路由。

处理过程如下:

  1. 顶级目录 /dashboard:

    1. 类型为 CATALOGUE。
    2. 没有父级目录,因此添加 Outlet 组件:
    appRoute.element = (
    <Suspense fallback={<CircleLoading />}>
      <Outlet />
    </Suspense>
    );
    
  2. 递归处理子权限:

    1. 子权限 "/dashboard/overview""/dashboard/stats" 被递归处理为子路由对象。
  3. 添加默认跳转:

    1. 为目录 “/dashboard” 添加一个索引路由,默认跳转到 “/dashboard/overview”:
    appRoute.children.unshift({
      index: true,
      element: <Navigate to="/dashboard/overview" replace />,
    });
    
if (!isEmpty(children)) {
 // 如果目中没有子权限,就设置一个默认路由,当访问目录路径时,自动挑战到第一个子权限对应的路由
  appRoute.children.unshift({
    index: true,
    element: <Navigate to={children[0].route} replace />,
  });
}

主要逻辑:

  1. 检查子路由是否为空:!isEmpty(children):检查当前目录下是否有子路由。
  2. 添加默认路由:如果有子路由,添加一个默认路由,使目录路径自动跳转到第一个子路由。element: <Navigate to={children[0].route} replace />:使用 Navigate 组件进行跳转。

最终路由结构(生成的路由结构可能类似于这种)

[
  {
    "path": "/dashboard",
    "element": "<Suspense fallback={<CircleLoading />}><Outlet /></Suspense>",
    "children": [
      {
        "index": true,
        "element": "<Navigate to='/dashboard/overview' replace />"
      },
      {
        "path": "/dashboard/overview",
        "element": "<Suspense fallback={<CircleLoading />}><OverviewComponent /></Suspense>",
        "meta": {
          "label": "Overview",
          "key": "/dashboard/overview"
        }
      },
      {
        "path": "/dashboard/stats",
        "element": "<Suspense fallback={<CircleLoading />}><StatsComponent /></Suspense>",
        "meta": {
          "label": "Statistics",
          "key": "/dashboard/stats"
        }
      }
    ],
    "meta": {
      "label": "Dashboard",
      "key": "/dashboard",
      "hideTab": true
    }
  }
]

PermissionType的值是

PermissionType = {
  0: "CATALOGUE", // 目录
  1: "MENU", // 菜单
  2: "BUTTON", // 按钮
  BUTTON: 2,
  CATALOGUE: 0,
  MENU: 1,
}

为什么存在多个 Outlet

  1. 每一级的 Outlet 只负责渲染当前层级的子路由:这保证了路由结构和组件结构的清晰和独立。
  2. 缺少 Outlet:如果某一级路由没有 Outlet,则其子路由无法渲染。子路由不会自动寻找祖先级的 Outlet 来插入。
  3. 设计原则:每一级只做每一级的事情。这种设计确保了每个组件的职责单一和明确,避免了复杂的依赖关系。

在我的观点里面:理论上我们设计了一套路由规则,并且在主文件中Main.tsx 设置了 Outlet ,此时无论是 /, /dashboard, /dashboard/overview, /dashboard/stats ... 其中的任何一个路由被命中之后,都应该注入到这个核心布局页面的Outlet中。虽然这样看似简单,但实际上会带来复杂度和维护性的问题非常严重。 当所有的子路由都注入到一个 Outlet 中,React Router 在解析和匹配路径时需要遍历整个路由树,查找每一个可能的路径匹配。这会导致以下问题:

  1. 复杂的路径解析:例如,当访问 /dashboard/analytics/reports 时,React Router 需要解析 /dashboard、/dashboard/analytics 和 /dashboard/analytics/reports。如果所有的子路由都在同一个 Outlet 中,路由解析器需要检查每个路径的每个可能组合,复杂度接近 O(n)。
  2. 路径冲突风险:多个路由在同一层级的 Outlet 中,容易产生路径冲突,尤其是动态路由和静态路由混合时。
  3. 所有的路由和组件都放在一个 Outlet 中,会导致组件职责不清晰:
  4. 难以维护:每个组件需要处理所有可能的子组件,导致职责分散,代码难以维护。
  5. 测试困难:每个组件需要处理不同层级的逻辑,增加了测试的难度。

如果使用分层的 Outlet,会有非常多的优势

  1. 分层解析:每一层路由只解析和匹配当前层级的路径。例如,AppLayout 处理 /dashboard,DashboardLayout 处理 /dashboard/analytics,AnalyticsLayout 处理 /dashboard/analytics/reports。这样每一层的复杂度都是 O(1),总复杂度降低到 O(n)。
  2. 路径清晰:每一层路径都有明确的处理组件,减少路径冲突风险。
  3. 浅层嵌套:每一层组件只处理当前层级的逻辑。例如,AppLayout 只处理顶层布局,DashboardLayout 只处理 /dashboard 的布局,AnalyticsLayout 只处理 /dashboard/analytics 的布局。
  4. 状态和上下文隔离:每一层组件只需处理当前层级的状态和上下文,简化了状态传递和管理。
  5. 职责单一:每个组件只处理当前层级的逻辑。例如,AppLayout 处理全局布局和导航,DashboardLayout 处理仪表板的布局和导航,AnalyticsLayout 处理分析页面的布局和导航。
  6. 易于维护和测试:每个组件的职责单一,逻辑清晰,代码易于维护和测试。

所以,理解为什么会存在多个 Outlet 非常重要,对理解整体路由架构非常有益。

PermissionType 枚举的三个类型(目录、菜单、按钮)分别代表着什么?

1. 目录 (CATALOGUE) 作用:目录权限通常用来组织其他权限项,它本身不对应具体的功能页面,而是一个容器,包含其他菜单项或子目录。 用途:主要用于构建多级导航结构,使得权限结构清晰、层次分明。 示例:一个目录可能包含多个子菜单或子目录,例如“Dashboard”目录下包含“Overview”和“Stats”菜单。、 核心目的

  1. 层次组织:目录的主要作用是组织和划分权限结构,形成层级关系。它本身不对应具体的功能页面。
  2. 占位符:在路由规则中添加 Outlet,为其子路由提供一个渲染的占位符。 2. 菜单 (MENU) 作用:菜单权限对应具体的功能页面或视图,当用户点击菜单项时,会导航到相应的页面。 用途:用于定义应用中的功能页面,用户通过菜单项进行导航。 示例:例如,“Overview”菜单项对应一个统计概览页面,“Stats”菜单项对应一个详细统计页面。 核心目的:
  3. 具体功能页面:菜单项对应具体的功能页面或视图,当用户点击菜单项时,会导航到相应的页面。
  4. 路由命中:当路径匹配时,加载对应的组件。 3. 按钮 (BUTTON) 作用:按钮权限对应页面中的具体操作按钮,控制用户是否可以执行某些操作(如新增、编辑、删除等)。 用途:用于细粒度地控制用户在页面上的操作权限,通常用于权限较细的系统中,例如管理系统中的操作按钮权限。 示例:在一个用户管理页面上,可能有“新增用户”、“编辑用户”、“删除用户”等按钮,按钮权限可以控制用户是否有权使用这些按钮。 核心目的:
  5. 具体操作权限:按钮项对应页面内的具体操作按钮,控制用户是否可以执行某些操作(如新增、编辑、删除等)。
  6. 细粒度控制:通常用于权限较细的系统中,例如管理系统中的操作按钮权限。

我们将 Outlet 和 PermissionType的三个类型搞明白之后,再来看这段代码就相对没有什么大的知识盲区了

if (type === PermissionType.MENU) {
   const Element = lazy(resolveComponent(component!) as any);
   if (frameSrc) {
     // 
     appRoute.element = <Element src={frameSrc} />;
   } else {
     appRoute.element = (
       <Suspense fallback={<CircleLoading />}>
         <Element />
       </Suspense>
     );
   }
 }

处理菜单类型的权限:

如果权限类型是 PermissionType.MENU,则处理为菜单路由。 如果有 frameSrc,则加载 iframe 组件。 否则,懒加载组件并使用 Suspense 包裹以显示加载指示器。

appRoute.element = <Element src={frameSrc} />;

如果某个菜单项的权限数据包含 frameSrc 属性,那么表示这个菜单项对应的是一个 iframe 链接,而不是一个常规的 React 组件。在这种情况下,应该将 frameSrc 的值作为 iframe 的 src 属性来加载对应的外部页面。

处理菜单类型的权限

  1. 懒加载组件:使用 lazyresolveComponent 动态导入菜单项对应的组件,实现懒加载。
  2. 如果菜单项包含 frameSrc,表示这是一个 iframe 链接,将其直接设置为组件的 src 属性。
  3. 否则,使用 Suspense 包裹懒加载的组件 Element,在加载组件时显示加载指示器 CircleLoading。

这段代码通过识别权限类型 (CATALOGUE 或 MENU),分别处理目录和菜单的路由配置。 目录用于组织子权限,设置 Outlet 以支持嵌套路由,并提供默认跳转。 菜单对应具体的功能页面,通过懒加载动态导入和渲染组件。这种设计使得路由配置清晰、结构化,并支持动态权限和高效的组件加载。

const permissions = useUserPermission(); // 获取用户权限数据
  return useMemo(() => {
    const flattenedPermissions = flattenTrees(permissions!); // 将权限树结构展平
    const permissionRoutes = transformPermissionToMenuRoutes(
      permissions || [],
      flattenedPermissions,
    ); // 转换权限数据为路由配置
    return [...permissionRoutes]; // 返回路由配置数组
  }, [permissions]); // 当 permissions 变化时,重新计算路由配置
}
  1. useUserPermission 是一个自定义 Hook,用于从全局状态或上下文中获取当前用户的权限数据。这个数据可能是一个嵌套的权限树结构。
  2. useMemo 是一个 React Hook,用于在依赖项(permissions)变化时重新计算值。这样可以避免不必要的重新计算,提高性能。
  3. flattenTrees 是一个函数,用于将嵌套的权限树结构展平成一个平面的数组。这样可以简化后续的处理逻辑。
  4. transformPermissionToMenuRoutes 是一个函数,用于将权限数据转换为路由配置。它接收两个参数:
    1. permissions:原始的权限数据(可能是嵌套的)。
    2. flattenedPermissions:展平后的权限数据。 这个函数会根据权限数据生成对应的路由配置。
  5. 最终返回生成的路由配置数组。使用扩展运算符 … 将其转换为一个新的数组实例。
const data = [
  {
    id: '9100714781927703',
    parentId: '',
    label: 'sys.menu.dashboard',
    name: 'Dashboard',
    icon: 'ic-analysis',
    type: 0,
    route: 'dashboard',
    order: 1,
    children: [
      {
        id: '8426999229400979',
        parentId: '9100714781927703',
        label: 'sys.menu.workbench',
        name: 'Workbench',
        type: 1,
        route: 'workbench',
        component: '/dashboard/workbench/index.tsx',
      },
      {
        id: '9710971640510357',
        parentId: '9100714781927703',
        label: 'sys.menu.analysis',
        name: 'Analysis',
        type: 1,
        route: 'analysis',
        component: '/dashboard/analysis/index.tsx',
      },
    ],
  },
  {
    id: '0901673425580518',
    parentId: '',
    label: 'sys.menu.management',
    name: 'Management',
    icon: 'ic-management',
    type: 0,
    route: 'management',
    order: 2,
    children: [
      {
        id: '2781684678535711',
        parentId: '0901673425580518',
        label: 'sys.menu.user.index',
        name: 'User',
        type: 0,
        route: 'user',
        children: [
          {
            id: '4754063958766648',
            parentId: '2781684678535711',
            label: 'sys.menu.user.profile',
            name: 'Profile',
            type: 1,
            route: 'profile',
            component: '/management/user/profile/index.tsx',
          },
          {
            id: '2516598794787938',
            parentId: '2781684678535711',
            label: 'sys.menu.user.account',
            name: 'Account',
            type: 1,
            route: 'account',
            component: '/management/user/account/index.tsx',
          },
        ],
      },
      {
        id: '0249937641030250',
        parentId: '0901673425580518',
        label: 'sys.menu.system.index',
        name: 'System',
        type: 0,
        route: 'system',
        children: [
          {
            id: '1985890042972842',
            parentId: '0249937641030250',
            label: 'sys.menu.system.organization',
            name: 'Organization',
            type: 1,
            route: 'organization',
            component: '/management/system/organization/index.tsx',
          },
          {
            id: '4359580910369984',
            parentId: '0249937641030250',
            label: 'sys.menu.system.permission',
            name: 'Permission',
            type: 1,
            route: 'permission',
            component: '/management/system/permission/index.tsx',
          },
          {
            id: '1689241785490759',
            parentId: '0249937641030250',
            label: 'sys.menu.system.role',
            name: 'Role',
            type: 1,
            route: 'role',
            component: '/management/system/role/index.tsx',
          },
          {
            id: '0157880245365433',
            parentId: '0249937641030250',
            label: 'sys.menu.system.user',
            name: 'User',
            type: 1,
            route: 'user',
            component: '/management/system/user/index.tsx',
          },
          {
            id: '0157880245365434',
            parentId: '0249937641030250',
            label: 'sys.menu.system.user_detail',
            name: 'User Detail',
            type: 1,
            route: 'user/:id',
            component: '/management/system/user/detail.tsx',
            hide: true,
          },
        ],
      },
    ],
  },
  {
    id: '2271615060673773',
    parentId: '',
    label: 'sys.menu.components',
    name: 'Components',
    icon: 'solar:widget-5-bold-duotone',
    type: 0,
    route: 'components',
    order: 3,
    children: [
      {
        id: '2478488238255411',
        parentId: '2271615060673773',
        label: 'sys.menu.icon',
        name: 'Icon',
        type: 1,
        route: 'icon',
        component: '/components/icon/index.tsx',
      },
      {
        id: '6755238352318767',
        parentId: '2271615060673773',
        label: 'sys.menu.animate',
        name: 'Animate',
        type: 1,
        route: 'animate',
        component: '/components/animate/index.tsx',
      },
      {
        id: '9992476513546805',
        parentId: '2271615060673773',
        label: 'sys.menu.scroll',
        name: 'Scroll',
        type: 1,
        route: 'scroll',
        component: '/components/scroll/index.tsx',
      },
      {
        id: '1755562695856395',
        parentId: '2271615060673773',
        label: 'sys.menu.markdown',
        name: 'Markdown',
        type: 1,
        route: 'markdown',
        component: '/components/markdown/index.tsx',
      },
      {
        id: '2122547769468069',
        parentId: '2271615060673773',
        label: 'sys.menu.editor',
        name: 'Editor',
        type: 1,
        route: 'editor',
        component: '/components/editor/index.tsx',
      },
      {
        id: '2501920741714350',
        parentId: '2271615060673773',
        label: 'sys.menu.i18n',
        name: 'Multi Language',
        type: 1,
        route: 'i18n',
        component: '/components/multi-language/index.tsx',
      },
      {
        id: '2013577074467956',
        parentId: '2271615060673773',
        label: 'sys.menu.upload',
        name: 'upload',
        type: 1,
        route: 'Upload',
        component: '/components/upload/index.tsx',
      },
      {
        id: '7749726274771764',
        parentId: '2271615060673773',
        label: 'sys.menu.chart',
        name: 'Chart',
        type: 1,
        route: 'chart',
        component: '/components/chart/index.tsx',
      },
    ],
  },
  {
    id: '8132044808088488',
    parentId: '',
    label: 'sys.menu.functions',
    name: 'functions',
    icon: 'solar:plain-2-bold-duotone',
    type: 0,
    route: 'functions',
    order: 4,
    children: [
      {
        id: '3667930780705750',
        parentId: '8132044808088488',
        label: 'sys.menu.clipboard',
        name: 'Clipboard',
        type: 1,
        route: 'clipboard',
        component: '/functions/clipboard/index.tsx',
      },
    ],
  },
  {
    id: '0194818428516575',
    parentId: '',
    label: 'sys.menu.menulevel.index',
    name: 'Menu Level',
    icon: 'ic-menulevel',
    type: 0,
    route: 'menu-level',
    order: 5,
    children: [
      {
        id: '0144431332471389',
        parentId: '0194818428516575',
        label: 'sys.menu.menulevel.1a',
        name: 'Menu Level 1a',
        type: 1,
        route: 'menu-level-1a',
        component: '/menu-level/menu-level-1a/index.tsx',
      },
      {
        id: '7572529636800586',
        parentId: '0194818428516575',
        label: 'sys.menu.menulevel.1b.index',
        name: 'Menu Level 1b',
        type: 0,
        route: 'menu-level-1b',
        children: [
          {
            id: '3653745576583237',
            parentId: '7572529636800586',
            label: 'sys.menu.menulevel.1b.2a',
            name: 'Menu Level 2a',
            type: 1,
            route: 'menu-level-2a',
            component: '/menu-level/menu-level-1b/menu-level-2a/index.tsx',
          },
          {
            id: '4873136353891364',
            parentId: '7572529636800586',
            label: 'sys.menu.menulevel.1b.2b.index',
            name: 'Menu Level 2b',
            type: 0,
            route: 'menu-level-2b',
            children: [
              {
                id: '4233029726998055',
                parentId: '4873136353891364',
                label: 'sys.menu.menulevel.1b.2b.3a',
                name: 'Menu Level 3a',
                type: 1,
                route: 'menu-level-3a',
                component: '/menu-level/menu-level-1b/menu-level-2b/menu-level-3a/index.tsx',
              },
              {
                id: '3298034742548454',
                parentId: '4873136353891364',
                label: 'sys.menu.menulevel.1b.2b.3b',
                name: 'Menu Level 3b',
                type: 1,
                route: 'menu-level-3b',
                component: '/menu-level/menu-level-1b/menu-level-2b/menu-level-3b/index.tsx',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    id: '9406067785553476',
    parentId: '',
    label: 'sys.menu.error.index',
    name: 'Error',
    icon: 'bxs:error-alt',
    type: 0,
    route: 'error',
    order: 6,
    children: [
      {
        id: '8557056851997154',
        parentId: '9406067785553476',
        label: 'sys.menu.error.403',
        name: '403',
        type: 1,
        route: '403',
        component: '/sys/error/Page403.tsx',
      },
      {
        id: '5095669208159005',
        parentId: '9406067785553476',
        label: 'sys.menu.error.404',
        name: '404',
        type: 1,
        route: '404',
        component: '/sys/error/Page404.tsx',
      },
      {
        id: '0225992135973772',
        parentId: '9406067785553476',
        label: 'sys.menu.error.500',
        name: '500',
        type: 1,
        route: '500',
        component: '/sys/error/Page500.tsx',
      },
    ],
  },
  {
    id: '3981225257359246',
    parentId: '',
    label: 'sys.menu.calendar',
    name: 'Calendar',
    icon: 'solar:calendar-bold-duotone',
    type: 1,
    route: 'calendar',
    component: '/sys/others/calendar/index.tsx',
  },
  {
    id: '3513985683886393',
    parentId: '',
    label: 'sys.menu.kanban',
    name: 'kanban',
    icon: 'solar:clipboard-bold-duotone',
    type: 1,
    route: 'kanban',
    component: '/sys/others/kanban/index.tsx',
  },
  {
    id: '5455837930804461',
    parentId: '',
    label: 'sys.menu.disabled',
    name: 'Disabled',
    icon: 'ic_disabled',
    type: 1,
    route: 'disabled',
    status: 0,
    component: '/sys/others/calendar/index.tsx',
  },
  {
    id: '7728048658221587',
    parentId: '',
    label: 'sys.menu.label',
    name: 'Label',
    icon: 'ic_label',
    type: 1,
    route: 'label',
    newFeature: true,
    component: '/sys/others/blank.tsx',
  },
  {
    id: '5733704222120995',
    parentId: '',
    label: 'sys.menu.frame',
    name: 'Frame',
    icon: 'ic_external',
    type: 0,
    route: 'frame',
    children: [
      {
        id: '9884486809510480',
        parentId: '5733704222120995',
        label: 'sys.menu.external_link',
        name: 'External Link',
        type: 1,
        route: 'external_link',
        hideTab: true,
        component: '/sys/others/iframe/external-link.tsx',
        frameSrc: 'https://ant.design/',
      },
      {
        id: '9299640886731819',
        parentId: '5733704222120995',
        label: 'sys.menu.iframe',
        name: 'Iframe',
        type: 1,
        route: 'frame',
        component: '/sys/others/iframe/index.tsx',
        frameSrc: 'https://ant.design/',
      },
    ],
  },
  {
    id: '0941594969900756',
    parentId: '',
    label: 'sys.menu.blank',
    name: 'Disabled',
    icon: 'ic_blank',
    type: 1,
    route: 'blank',
    component: '/sys/others/blank.tsx',
  },
];

综合来看里面入参是,嵌套的用户权限树结构,出参是根据权限树结构按照一定的规则转换成的路由规则。这里面最重要的就是 目录和菜单的独立处理。 下一步重点是,研究嵌套的权限数据结构,了解为什么要用这种结构

数据结构

基本字段 id: 权限项的唯一标识符。 parentId: 父权限项的唯一标识符。顶级权限项的 parentId 为空。 label: 权限项的标签,通常用于显示在界面上。 name: 权限项的名称。 icon: 权限项的图标。 type: 权限项的类型。0 表示目录(CATALOGUE),1 表示菜单(MENU)。 route: 权限项对应的路由路径。 order: 权限项的排序顺序。 component: 权限项对应的组件路径。 frameSrc: 如果权限项对应外部链接,表示 iframe 的源地址。 hide: 是否隐藏该权限项。 newFeature: 是否是新特性。 嵌套结构 children: 子权限项的数组,表示嵌套的层级结构。 嵌套数据结构的设计目的 目录(type: 0)

  • 用于组织和划分权限结构,形成层次关系。
  • 目录本身不对应具体的页面,而是用于包含子权限项(菜单或其他目录)。 菜单(type: 1)
  • 对应具体的功能页面或视图。
  • 当用户点击菜单项时,会导航到相应的页面。

将嵌套权限数据转换为路由配置

展平权限树结构:将嵌套的树状结构转换为平面结构,以便更容易地进行处理和分析。 转换为路由配置:

  • 根据权限数据生成对应的路由配置。
  • 处理目录和菜单类型的权限,设置 Outlet 以支持嵌套路由,动态加载组件或创建 iframe 元素。