fetch+promise+FileReader封装资源下载

879 阅读2分钟

场景

项目开发中通常会涉及从 服务端下载文件 的需求,发起 ajax请求 后,服务器响应数据通常会有两种情况,第一种是返回 json数据 (一般用于抛出提示),第二种是返回 文件流数据

代码设计

为此,我封装了一个支持GET和POST请求的具有提示和下载功能的组件。代码如下:

import { Button, message } from 'antd';
import React from 'react';

type Props = {
  /**
   * @description 请求路径
   */
  url: string;
  /**
   * @description 请求参数
   */
  params: Record<string, any>;
  /**
   * @description 请求方式 , 默认 'GET'
   */
  method?: 'GET' | 'POST';
  /**
   * @description 下载文件名称 , 默认 'file'
   */
  fileName?: string;
  [props: string]: any;
};

const AjaxDownloadButton: React.FC<Props> = (props) => {
  const { url = '', params = {}, method = 'GET', fileName = 'file', children, ...rest } = props;

  const handleClick = () => {
    try {
      // GET请求的params打平为字符串
      const paramsStr = Object.keys(params)
        .map((key) => `${key}=${params[key]}`)
        .join('&');
      const ReqGETUrl = `${url}?${paramsStr}`;

      // 根据请求类型配置fetch请求
      const promise =
        method === 'GET'
          ? fetch(ReqGETUrl)
          : fetch(url, {
              method,
              body: JSON.stringify(params),
            });

      // 发起请求,并处理响应数据
      promise
        .then((response) => response.blob())
        .then((data) => {
          if (data.type === 'application/json') {
            // json数据处理
            const reader = new FileReader();
            reader.readAsText(data, 'utf-8');
            reader.onload = (e) => {
              const result = e.target?.result;
              if (typeof result === 'string') {
                const infoMsg = JSON.parse(result).retMsg;
                // 抛出响应数据中的提示信息
                message.info(infoMsg);
              }
            };
          } else {
            //其余类型都当成文件下载
            const a = document.createElement('a');
            a.href = window.URL.createObjectURL(data);
            a.download = fileName;
            a.click();
          }
        })
        .catch(() => {
          message.error('请求错误!');
        });
    } catch (e) {
      message.error('请求错误!');
    }
  };
  return (
    <Button {...rest} onClick={handleClick}>
      {children}
    </Button>
  );
};

export default AjaxDownloadButton;

组件效果

1.服务端响应数据为文件流数据

先mock出文件流数据

import fetchMock from 'fetch-mock';

fetchMock.mock('example/success/download?success=success', () => '下载成功!');

效果如下:

下载成功.png

2.服务端响应数据为json数据

先mock出json数据

import fetchMock from 'fetch-mock';

fetchMock.mock('example/info/download?info=info', () => ({
  data: [],
  retCode: 'T100',
  retMsg: '下载失败,请联系相关人员!',
}));

效果如下:

下载提示.png

3.请求响应异常

先mock出异常数据

import fetchMock from 'fetch-mock';


fetchMock.mock('example/error/download?error=error', () => {
  throw new Error('下载错误!');
});

下载错误.png

FAQ

1、为什么用fetch发起请求?

流行框架的请求插件底层一般是基于fetch封装的,但是有时候框架的请求插件配置会约束我们的设计。比如,我开发的项目其实是基于umi框架的,正常情况下使用 @umijs/plugin-request 的 useRequest去发起请求是很方便的,还会内置提示和错误抛出功能,但是像下载文件这种请求比较特殊,会有两种返回数据格式,因为也需要为了这个需求,去app.ts文件对@umijs/plugin-request进行请求成功情况的配置,不然文件流就会被当成错误提示抛出。所以为了组件更加通用性,选择了用原生的fetch发起请求。