Ant Design Pro快速入门——后端集成

纯前端菜鸡,只有一些HTML、CSS和JavaScript的基础,最近配合的前端跑路了,想用Ant Design Pro来快速搭建一套中后台来管理数据,只能自己上了,主要自己也想试试水,从后端的视角看前端,整体偏向于实战。

后端集成

中后台最常用的就是数据处理和数据交互,与后端的CURD接口交互不可避免,那Ant Design Pro如何与后端交互呢?

openAPI

Pro中提供了openAPI的插件,用于快速生成API请求代码,其核心功能如下:

  • 自动生成API请求代码:根据后端提供的OpenAPI文档,自动生成前端的services/API.ts文件
  • 集成请求工具:默认使用umi的request工具,可配置axios或其他请求工具
  • Mock数据支持:可以直接使用OpenAPI 文档生成Mock数据,无需手动编写
  • 类型安全:生成的API代码包含完整的类型定义,类型检查更 strict。

关于这个功能,怎么说吧,看起来是一件很cool的事情,但在实际的开发过程中,你很难要求开发按何种方式来管理文档,所以有兴趣可以自行了解openAPI,我个人认为还是手动写请求比较实在。当然,这并不是重点,重点是如果你在项目中配置了 @umijs/plugin-openapi 并运行生成命令(如 umi openapi)时,插件会:

src/services/
├── ant-design-pro/  # 默认生成的 API 文件(通常对应业务接口)
│   ├── api.ts       # 自动生成的 API 请求函数
│   └── typings.d.ts # 自动生成的类型定义
├── swagger/         # 从 Swagger 生成的 API 文件(可能用于基础服务)
│   ├── user.ts
│   └── ...

所以,不需要把自定义的请求放到这两个目录的文件中,如果后端 Swagger 文档更新后重新生成代码,手动修改的内容会被覆盖,我初学时就是把一些代码定义到了api.tstypings.d.ts中,然后跑了npm run openapi自动生成文档,写的逻辑全被自动覆盖了……

网络请求

Pro总结出一套标准的接口结构规范,并提供统一的接口解析、错误处理的能力。

使用Request

通过import { request } from '@umijs/max';可是使用内置的request方法,该方法有两个参数,第一个是url,第二个是配置项,配置项中可以包含请求参数,请求方法,请求头等,示例如下:

// @ts-ignore
/* eslint-disable */
import { request } from '@umijs/max';

/** 获取规则列表 GET /api/rule */
export async function rule(
  params: {
    // query
    /** 当前的页码 */
    current?: number;
    /** 页面的容量 */
    pageSize?: number;
  },
  options?: { [key: string]: any },
) {
  return request<WorkFlowAPI.RuleList>('/api/rule', {
    method: 'GET',
    params: {
      ...params,
    },
    ...(options || {}),
  });
}

使用useRequest

useRequest是一个Hooks,用于请求数据,它封装了request方法,提供了一些常用的功能,如loading、error、data等,示例如下:

import { useRequest } from '@umijs/max';

//自动管理请求状态
export default () => {
  const { data, error, loading } = useRequest('/api/user');
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>Username: {data.name}</div>;
};

/******************************分隔线************************************/

//手动触发请求,适合表单提交等场景
const { run } = useRequest(
  (params) => fetch('/api/search', { method: 'POST', body: params }),
  { manual: true } // 手动触发
);

// 点击按钮时发送请求
<button onClick={() => run({ keyword: 'test' })}>Search</button>

/******************************分隔线************************************/

// 监听依赖项变化
const [keyword, setKeyword] = useState('');
const { data } = useRequest('/api/search', {
  refreshDeps: [keyword], // keyword 变化时重新请求
});


/******************************分隔线************************************/
// 轮询请求,每 3 秒请求一次
const { data } = useRequest('/api/stock-price', {
  pollingInterval: 3000,
});

/******************************分隔线************************************/
// 缓存请求结果,5秒内直接读缓存,避免重复的相同请求
const { data } = useRequest('/api/config', {
  cacheKey: 'global-config', // 缓存键
  staleTime: 5000, // 5秒内直接读缓存
});

中间件

如果我们想在请求的过程中插入自定义逻辑,如:

  • ​修改请求参数​(如添加全局 token)
  • ​统一处理错误​(如接口报错时弹出通知)
    ​- 日志记录​(记录请求耗时)
  • ​Mock 数据拦截​(开发环境下模拟响应)

全局中间件

在 src/app.tsx 中配置全局中间件:

// src/app.ts
export const request = {
  middlewares: [
    // 示例:添加请求头
    async (ctx, next) => {
      ctx.req.options.headers = {
        ...ctx.req.options.headers,
        Authorization: 'Bearer xxx',
      };
      await next(); // 继续执行下一个中间件或发送请求
    },

    // 示例:统一错误处理
    async (ctx, next) => {
      try {
        await next();
      } catch (error) {
        console.error('Request failed:', error);
        throw error; // 继续抛出错误,由 useRequest 的 error 状态捕获
      }
    },
  ],
};

单次中间件

const { data } = useRequest('/api/data', {
  middlewares: [
    async (ctx, next) => {
      console.log('请求参数:', ctx.params); // 打印请求参数
      await next();
      console.log('响应数据:', ctx.data);  // 打印响应数据
    },
  ],
});

拦截器

拦截器也可以达到同样的效果。

前置拦截

在网络请求的 .then 或 catch 处理前拦截,你可以在 src/app.tsx 网络请求配置内增加如下配置:

export const request: RequestConfig = {
  // 新增自动添加AccessToken的请求前拦截器
  requestInterceptors: [authHeaderInterceptor],
};

//拦截逻辑
const authHeaderInterceptor = (url: string, options: RequestConfig) => {
  const authHeader = { Authorization: 'Bearer xxxxxx' };
  return {
    url: `${url}`,
    options: { ...options, interceptors: true, headers: authHeader },
  };
};

后置拦截

后置拦截一般用来处理异常,你可以在 src/app.tsx 网络请求配置内增加如下配置:

// src/app.tsx
const demoResponseInterceptors = (response: Response, options: RequestConfig) => {
  response.headers.append('interceptors', 'yes yo');
  return response;
};

export const request: RequestConfig = {
  responseInterceptors: [demoResponseInterceptors],
};

跨域请求

跨域请求,相比大家并不陌生,比较常见的解决方案就是JsonP和CORS,我比较感兴趣的是这个proxy的解决方案,它是通过本地代理服务器将前端请求转发给后端,流程如下:

  1. ​前端请求​ → 发送到本地开发服务器(如 http://localhost:8000/api/users)。
  2. ​代理服务器​ → 将请求转发到真实后端(如 http://api.example.com/api/users)。
  3. ​后端响应​ → 代理服务器将结果返回给前端。

修改config/proxy.ts文件:


export default {
  '/api': {
    target: 'https://jsonplaceholder.typicode.com', // 免费测试 API
    changeOrigin: true,      // 修改请求头 Host
    pathRewrite: { '^/api': '' }, // 去掉 /api 前缀
  }
};

请求示例:

import { useRequest } from 'umi';
import { useState, useEffect } from 'react';

export default () => {
  // 示例 1:获取用户列表(代理到 https://jsonplaceholder.typicode.com/users)
  const { data, loading } = useRequest('/api/users');

  const [content, setContent] = useState<string>('加载中...'); // 初始化内容

  useEffect(() => {
    const fetchData = async () => {
      try {
        const rep = await fetch('/api/users');
        const text = await rep.text();
        setContent(text);
      } catch (error) {
        console.error('获取日志失败:', error);
        setContent('加载内容失败');
      }
    };

    fetchData();
  }, []);

  return (
    <div>
      {content}
    </div>
  );
};

MOCK请求

在 Ant Design Pro 中,​Mock 请求是一种在开发阶段模拟后端 API 返回数据的技术,无需依赖真实后端服务即可进行前端开发和测试:

  1. ​独立开发​:前端无需等待后端接口完成
  2. ​快速原型​:模拟各种响应(成功/失败/异常)
  3. ​联测试​:生成随机数据测试页面逻辑
  4. ​无跨域问题​:本地服务直接返回数据

在很多情况下前端是在后端还没有开发完成之前就开始开发的,这时候我们就需要用到 mock 数据了。Pro 中约定了两种 mock 的定义方式。

  • 在根目录的 mock 中接入
  • 在 src/page 中的 mock.ts 的文件中配置
// 代码中会兼容本地 service mock 以及部署站点的静态数据
export default {
  // 支持值为 Object 和 Array
  'GET /api/currentUser': (req: Request, res: Response) => {
    if (!getAccess()) {
      res.status(401).send({
        data: {
          isLogin: false,
        },
        errorCode: '401',
        errorMessage: '请先登录!',
        success: true,
      });
      return;
    }
    res.send({
      success: true,
      data: {
        name: 'Serati Ma',
        avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
        userid: '00000001',
        email: 'antdesign@alipay.com',
        signature: '海纳百川,有容乃大',
        title: '交互专家',
        group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
        tags: [
          {
            key: '0',
            label: '很有想法的',
          },
          {
            key: '1',
            label: '专注设计',
          },
          {
            key: '2',
            label: '辣~',
          },
          {
            key: '3',
            label: '大长腿',
          },
          {
            key: '4',
            label: '川妹子',
          },
          {
            key: '5',
            label: '海纳百川',
          },
        ],
        notifyCount: 12,
        unreadCount: 11,
        country: 'China',
        access: getAccess(),
        geographic: {
          province: {
            label: '浙江省',
            key: '330000',
          },
          city: {
            label: '杭州市',
            key: '330100',
          },
        },
        address: '西湖区工专路 77 号',
        phone: '0752-268888888',
      },
    });
  },
  // GET POST 可省略
  'GET /api/users': [
    {
      key: '1',
      name: 'John Brown',
      age: 32,
      address: 'New York No. 1 Lake Park',
    },
    {
      key: '2',
      name: 'Jim Green',
      age: 42,
      address: 'London No. 1 Lake Park',
    },
    {
      key: '3',
      name: 'Joe Black',
      age: 32,
      address: 'Sidney No. 1 Lake Park',
    },
  ],
  'POST /api/login/account': async (req: Request, res: Response) => {
    const { password, username, type } = req.body;
    await waitTime(2000);
    if (password === '123456' && username === 'admin') {
      res.send({
        status: 'ok',
        type,
        currentAuthority: 'admin',
      });
      access = 'admin';
      return;
    }
    if (password === '123456' && username === 'user') {
      res.send({
        status: 'ok',
        type,
        currentAuthority: 'user',
      });
      access = 'user';
      return;
    }
    if (type === 'mobile') {
      res.send({
        status: 'ok',
        type,
        currentAuthority: 'admin',
      });
      access = 'admin';
      return;
    }

    res.send({
      status: 'error',
      type,
      currentAuthority: 'guest',
    });
    access = 'guest';
  },
  'POST /api/login/outLogin': (req: Request, res: Response) => {
    access = '';
    res.send({ data: {}, success: true });
  },
  'POST /api/register': (req: Request, res: Response) => {
    res.send({ status: 'ok', currentAuthority: 'user', success: true });
  },
  'GET /api/500': (req: Request, res: Response) => {
    res.status(500).send({
      timestamp: 1513932555104,
      status: 500,
      error: 'error',
      message: 'error',
      path: '/base/category/list',
    });
  },
  'GET /api/404': (req: Request, res: Response) => {
    res.status(404).send({
      timestamp: 1513932643431,
      status: 404,
      error: 'Not Found',
      message: 'No message available',
      path: '/base/category/list/2121212',
    });
  },
  'GET /api/403': (req: Request, res: Response) => {
    res.status(403).send({
      timestamp: 1513932555104,
      status: 403,
      error: 'Forbidden',
      message: 'Forbidden',
      path: '/base/category/list',
    });
  },
  'GET /api/401': (req: Request, res: Response) => {
    res.status(401).send({
      timestamp: 1513932555104,
      status: 401,
      error: 'Unauthorized',
      message: 'Unauthorized',
      path: '/base/category/list',
    });
  },

  'GET  /api/login/captcha': getFakeCaptcha,
};

有兴趣的可以看看语雀上的教程:Ant Design 实战教程(beta 版)

posted @ 2025-05-07 16:31  破落户儿  阅读(177)  评论(0)    收藏  举报