让你的 Umi V4 和 Ant Design Pro 变身超级英雄:实现权限管理与后端路由的奇妙之旅

514 阅读3分钟

前言

想象一下,如果你的应用能像超级英雄一样,识别谁是“好人”,谁是“坏人”,并根据他们的身份决定他们能看到什么内容,那将是多么酷炫的体验!在这篇文章中,我们将一起探索如何利用 Umi V4 的强大功能和 Ant Design Pro 的优雅设计,构建一个既安全又灵活的权限管理系统。

代码实战

初始化:Umi V4贴心的集成了@ant-design/pro-components/pro-layout,只需开启配置中的layout选项即可。快速搭建后台架构,初始化项目选择Ant Design Pro为模板

npx create-umi@latest

mock后端数据:定义后端返回数据结构,直接使用umi提供的mock功能。格式如下:

mock/route.ts

export default {
  'GET /api/v1/routes': (req, res) => {
    res.json({
      success: true,
      data: [
        {
          menuId: 'home',
          parentId: null,
          enable: true,
          name: '首页',
          sort: 1000,
          path: '/home',
          direct: true,
        },
        {
          menuId: 'access_management',
          parentId: null,
          enable: true,
          name: '权限演示',
          sort: 2000,
          path: '/access',
          direct: false,
        },
        {
          menuId: 'user_management',
          parentId: 'access_management',
          enable: true,
          name: '用户',
          sort: 2001,
          path: '/access/user',
          direct: false,
        },
        {
          menuId: 'role_management',
          parentId: 'access_management',
          enable: true,
          name: '管理员',
          sort: 2002,
          path: '/access/role',
          direct: false,
          // 测试权限功能
          access: 'canSeeAdmin',
        },
      ],
      errorCode: 0,
    });
  },
};

解析路由数据:首先梳理下执行流程,用户登录→路由解析→跳转首页。这里存在一个问题,页面刷新后文件执行顺序是:global.(t/j)sx→app.(t/j)sx/parseRoutes,但跳转首页是路由跳转,页面并不会刷新,所以需要在login页面手动触发页面刷新并且需要判断登录态跳转,再依次执行组装路由跳转等

当然如果你获取路由数据的接口不需要鉴权,那你完全不用在global中执行,可以直接在parseRoutes方法中进行请求,相对应login页面也不需刷新页面,直接跳转即可

global.(t/j)sx

try {
  // 判断是否登录
  if (localStorage.getItem(TOKEN) !== null) {
   const { data: routesData } = await fetch('/api/v1/routes', {
     method: 'GET',
     headers: { 'Content-Type': 'application/json',Authorization: localStorage.getItem(TOKEN) },
  }).then((res) => res.json());
  if (routesData) {
    window.dynamic_routes = routesData;
  }
 }
} catch (err) {
  console.error(err);
}

export {};

app.(t/j)sx

export function patchRoutes({ routes, routeComponents }) {
  if (window.dynamic_routes) {
    const currentRouteIndex = Object.keys(routes).length;
    const parsedRoutes = parseRoutes(
      window.dynamic_routes,
      currentRouteIndex,
    );
    Object.assign(routes, parsedRoutes.routes);
    Object.assign(routeComponents, parsedRoutes.routeComponents);
  }
}

utils/route.(t/j)sx

function parseRoutes(routesRaw, beginIdx: number) {
  const routes = {};
  const routeComponents = {};
  const routeParentMap = new Map<string, number>();

  let currentIdx = beginIdx;

  routesRaw.forEach((route) => {
    let effectiveRoute = true;

    const formattedRoutePath = route.path;
    const routePath = generateRoutePath(formattedRoutePath);
    const componentPath = generateComponentPath(formattedRoutePath);
    const filePath = generateFilePath(formattedRoutePath);

    if (route.direct) {
      const tempRoute = {
        id: currentIdx.toString(),
        parentId: 'ant-design-pro-layout',
        name: route.name,
        path: routePath,
        file: filePath,
        access: route.access,
      }; 
      routes[currentIdx] = tempRoute;
      const tempComponent = lazy(() => import(`@/pages/${componentPath}`));
      routeComponents[currentIdx] = tempComponent;
    } else {
      // 判断是否非一级路由
      if (!route.parentId) {
        // 正在处理的为一级路由
        const tempRoute = {
          id: currentIdx.toString(),
          parentId: 'ant-design-pro-layout',
          name: route.name,
          path: routePath,
          access: route.access,
        };
        routes[currentIdx] = tempRoute;
        const tempComponent = Outlet;
        routeComponents[currentIdx] = tempComponent;
        routeParentMap.set(route.menuId, currentIdx);
      } else {
        // 非一级路由
        // 获取父级路由ID
        const realParentId = routeParentMap.get(route.parentId);
        if (realParentId) {
          const tempRoute = {
            id: currentIdx.toString(),
            parentId: realParentId.toString(),
            path: routePath,
            access: route.access,
            ...(route.redirect
              ? { redirect: route.redirectUrl }
              : { name: route.name, file: filePath }),
          };
          routes[currentIdx] = tempRoute;
          const tempComponent = route.redirect
            ? Outlet
            : lazy(() => import(`@/pages/${componentPath}`));
          routeComponents[currentIdx] = tempComponent;
        } else {
          // 找不到父级路由,路由无效,workingIdx不自增
          effectiveRoute = false;
        }
      }
    }

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

  return {
    routes,
    routeComponents,
  };
}

login.(t/j)sx

 const onFinish = async (values) => {
    try {
      if (values) {
        const msg = await login(values);
        if (msg?.token) {
          localStorage.setItem(TOKEN, msg.token);
          messageApi.success('登录成功!');
          window.location.reload();
          return;
        }
        messageApi.error(msg?.msg || '登录失败,请重试!');
      }
    } catch (error) {
      messageApi.error('登录失败,请重试!');
    }
  };

  if (localStorage.getItem(TOKEN) !== null) {
    return <Navigate to="/" replace />;
  }

关于权限

已采用后端路由方式,权限问题其实已经不需要前端来做什么,后端只需根据登录用户权限返回对应的路由即可。但前端也可以实现,直接使用Umi提供的access方案,不仅可以实现权限路由,甚至到按钮级别。直接调整接口access字段及以下文件测试即可,parseRoutes已具备功能。

src/access.ts

export default (initialState) => {
 // 此处数据由登录时存入 
 const { userId, role } = initialState;
  return {
    canSeeAdmin: role === 'admin',
  };
};

想实现按钮级权限,直接根据文档调用useAccess即可

import { PageContainer } from '@ant-design/pro-components';
import { Access, useAccess } from '@umijs/max';
import { Button } from 'antd';

const AccessPage: React.FC = () => {
  const access = useAccess();
  return (
    <PageContainer
      ghost
      header={{
        title: '权限示例',
      }}
    >
      <Access accessible={access.canSeeAdmin}>
        <Button>只有 Admin 可以看到这个按钮</Button>
      </Access>
    </PageContainer>
  );
};

export default AccessPage;

最后,如果你想要hash路由,更改config.(t/j)s文件中history选项即可

history: { type: 'hash' }

存在问题

1.global执行过早,umi插件未初始化。导致request配置获取不到,本地开发受限制会产生跨域。但本地开发可以启用umi的mock功能

注:标题和前言是cursor生成的,后续有内容再加