UmiJS PC 项目二:动态路由配置

693 阅读3分钟

UmiJS PC 项目一:ProTable 高度设置
UmiJS PC 项目二:动态路由配置
UmiJS PC 项目三:TypeScript 使用

在 PC 项目开发里,路由的使用与配置始终是关键要点,尤其是实际项目中动态路由的引入,进一步提升了其复杂性。

当我们运用脚手架搭建 vue 或 react 项目时,社区里存在诸多成熟的解决方案可供选用。然而,在使用 UmiJS 时,却会遭遇一些棘手的问题。基于此,结合官方文档、参考他人的经验以及实际的操作过程,我将为大家分享两种行之有效的解决方案。

一、基于权限 的动态路由方案

这种方案在官方文档中已给出详尽的使用步骤,此处便不再赘述,建议大家参考示例源码,或者亲自上手实践。

路由重定向处理

不过在实际运用过程中,若我们配置了 redirect,而权限配置却将该路由过滤掉,那么跳转时就会出现问题。解决办法是在动态配置的 patchRoutes 中,重新构建路由的 redirect 配置,以此确保路由跳转的正常运行。

  1. routes 配置 .umirc.ts
  2. 权限处理 access.ts
  3. app.ts 运行时配置
import { rebuildRedirect } from "./dynamicRoutes";

export function patchRoutes({ routes }: DynamicRoutes.ParseRoutesReturnType) {
  rebuildRedirect(routes);
}
  1. rebuildRedirect 核心处理文件
graph LR 
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px 
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px 
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px 
    A([开始]):::startend --> B(获取权限控制信息):::process 
    B --> C(权限过滤: 删除无访问权限的路由):::process 
    C --> D(获取第一个动态路由):::process 
    D --> E(路由分类):::process 
    E --> E1(无重定向路由):::process 
    E --> E2(有重定向路由):::process 
    E2 --> F{是否找到父级路由?}:::decision 
    F -->|是| G(更新重定向路径):::process 
    F -->|否| H(标记为待删除并移除父级 ID):::process 
    G --> I(删除无效路由):::process 
    H --> I 
    I --> J(更新根路径重定向):::process 
    J --> K([结束]):::startend

关键步骤:

  1. 权限过滤,移除无权限访问的路由。
  2. 路由分类,分为无重定向路由、有重定向路由。
  3. 处理重定向路由,更新重定向路径或标记为待删除。
  4. 删除无效路由。
  5. 更新根路径的重定向。
import access from "@/access";
import type { DynamicRoutes } from "./dynamicRoutes.d";
import { find } from "lodash-es";

export function rebuildRedirect(
  routes: DynamicRoutes.ParsedRoutes,
  baseRouteIdx = 2
) {
  const accessControl: Record<string, boolean> = access();
  // 删除没有访问权限的路由
  Object.keys(routes).forEach((key: any) => {
    let r = routes[key] as any;
    if (r.access && !accessControl[r.access]) {
      delete routes[key];
    }
  });
  const firstRouteKey = Object.keys(routes)[baseRouteIdx];
  const firstDynamicRoute = routes[firstRouteKey];

  const nonRedirectRoutes: any[] = [];
  const redirectRoutes: any[] = [],
    routesToDelete: any[] = [];

  // 分类路由
  Object.keys(routes).forEach((key: any) => {
    const k = Number(key);
    let r = routes[key] as any;
    if (k && !r.redirect && r.parentId) {
      nonRedirectRoutes.push(r);
    } else if (k && r.redirect && r.parentId) {
      redirectRoutes.push(r);
    }
  });

  // 处理重定向路由
  redirectRoutes.forEach((route: any) => {
    const parentRoute = find(
      nonRedirectRoutes,
      (r: any) => r.parentId === route.parentId
    );
    if (!parentRoute) {
      routesToDelete.push(route.id);
      delete route["parentId"];
    } else {
      route.redirect = parentRoute.path;
    }
  });

  // 删除无效路由
  routesToDelete.forEach((id: any) => {
    delete routes[id];
  });

  // 更新根路径的重定向
  Object.keys(routes).forEach((key: any) => {
    let r = routes[key] as any;
    if (r.path === "/" && r.redirect) {
      r.redirect = firstDynamicRoute.path;
    }
  });
}

二、服务端响应数据动态更新路由

GitHub 示例源码

路由响应 mock 数据

mock/dynamicRoutes.ts

截屏2025-02-05 16.38.41.png

生成动态路由数据及组件

该函数的主要目的是将原始的路由配置数据解析成包含路由信息和 React 组件的对象。其核心步骤包括初始化数据结构、遍历原始路由数据、根据是否为一级路由进行不同处理,同时处理组件加载,最后返回解析结果。

export function parseRoutes(
  routesRaw: DynamicRoutes.RouteRaw[],
  beginIdx: number
): DynamicRoutes.ParseRoutesReturnType {
  const routes: DynamicRoutes.ParsedRoutes = {}; // 转换后的路由信息
  const routeComponents: DynamicRoutes.ParsedRouteComponent = {}; // 生成的React.lazy组件
  const routeParentMap = new Map<string, number>(); // menuId 与路由记录在 routes 中的键 的映射。如:'role_management' -> 7

  let currentIdx = beginIdx; // 当前处理的路由项的键。把 patchRoutes 传进来的 routes 看作一个数组,这里就是元素的下标。

  routesRaw.forEach((route) => {
    let effectiveRoute = true; // 当前处理中的路由是否有效
    const routePath = route.path; // 全小写的路由路径
    const componentPath = route.component; // 组件路径 不含 @/pages/

    // 是否为直接显示(不含子路由)的路由记录,如:/home; /Dashboard
    if (!route.parentId) {
      // 生成路由信息
      const tempRoute: DynamicRoutes.Route = {
        id: currentIdx.toString(),
        parentId: "@@/global-layout",
        name: route.name,
        path: routePath,
        icon: route.icon,
      }; // 存储路由信息
      if (route.redirect) {
        tempRoute["redirect"] = route.redirect;
      }
      routes[currentIdx] = tempRoute; // 生成组件

      const tempComponent = route.component
        ? lazy(() => import(`@/pages/${componentPath}`))
        : Outlet; // 存储组件
      routeComponents[currentIdx] = tempComponent;
      routeParentMap.set(route.menuId, currentIdx);
    } else {
      // 非一级路由
      // 获取父级路由ID
      const realParentId = routeParentMap.get(route.parentId);

      if (realParentId) {
        // 生成路由信息
        const tempRoute: DynamicRoutes.Route = {
          id: currentIdx.toString(),
          parentId: realParentId.toString(),
          name: route.name,
          path: routePath,
        }; // 存储路由信息
        if (route.redirect) {
          tempRoute["redirect"] = route.redirect;
        }
        routes[currentIdx] = tempRoute; // 生成组件

        const tempComponent = componentPath
          ? lazy(() => import(`@/pages/${componentPath}`))
          : Outlet; // 存储组件
        routeComponents[currentIdx] = tempComponent;
      } else {
        // 找不到父级路由,路由无效,workingIdx不自增
        effectiveRoute = false;
      }
    }

    if (effectiveRoute) {
      // 当路由有效时,将workingIdx加一
      currentIdx += 1;
    }
  });

  return {
    routes,
    routeComponents,
  };
}

app.ts 运行时配置

import type { DynamicRoutes } from "./dynamicRoutes.d";
import { parseRoutes } from "./dynamicRoutes";

async function fetchDynamicRoutes() {
  try {
    const role = getRole();
    if (!role) return;
    const { data: routesData } = await fetch(`/api/system/routes/${role}`, {
      method: "POST",
    }).then((res) => res.json());
    if (routesData) {
      window.dynamicRoutes = routesData;
    }
  } catch {
    message.error("路由加载失败");
  }
}
await fetchDynamicRoutes();

export function patchRoutes({
  routes,
  routeComponents,
}: DynamicRoutes.ParseRoutesReturnType) {
  if (window.dynamicRoutes) {
    const routeKeys = Object.keys(routes)
      .filter((key) => parseInt(key) > 0)
      .map(parseInt);
    const beginIdx = routeKeys[routeKeys.length - 1] + 1;
    const parsedRoutes = parseRoutes(window.dynamicRoutes, beginIdx);
    Object.assign(routes, parsedRoutes.routes); // 直接操作原对象,合并路由数据
    Object.assign(routeComponents, parsedRoutes.routeComponents); // 合并组件
  }
}

参考:

Umi4 从零开始实现动态路由、动态菜单