浅谈 customRequest with umi-request 监听上传进度

浅谈 customRequest with umi-request 监听上传进度

由于 umi-request 是基于 fetch 实现,但fetch 并不像 axios或(xhr自带的upload.onProgress)一样具备现成的 onUploadProgress API监听文件上传处理进度的事件 progressEvent,但我们可以利用 umi-request-progress中间件 一样可以处理进度监听。

简单实现

upload.ts 定义统一上传请求方法

import umiRequest from 'umi-request';
import progressMiddleware from 'umi-request-progress';
// 注册进度监听内核中间件
umiRequest.use(progressMiddleware, { core: true });
/**
 * 上传文件
 * @param data FormData
 * @param onProgress 上传进度回调
 * @param path 上传路径
 */
export function uploadService(
  data: FormData,
  {
    onProgress,
    path,
    method = 'post',
  }: { onProgress: ({ percent }: { percent: number }) => void; path: string; method?: string },
) {
  return request<{
    data: any;
  }>(path, {
    method,
    data,
    onReqProgress: function ({ loaded, total }: ProgressEvent) {
    onProgress({ percent: parseInt(Math.round((loaded / total) * 100).toFixed(2)) });
    },
    requestType: 'form',
  });
}

uploader.tsx customRequest自定义上传逻辑处理

import { ContainerOutlined, DeleteOutlined, PaperClipOutlined } from '@ant-design/icons';
import { useRequest, useUpdateEffect } from 'ahooks';
import { Button, message, Modal, Progress, Spin, Tooltip, Upload } from 'antd';
import React, { useCallback, useState } from 'react';
import styles from './index.module.less';
import { uploadService } from '@/services/upload';
import type { RcFile } from 'antd/lib/upload';

const fileTypeList = [
  'application/vnd.ms-excel',
  ...
];

const Uploader: React.FC<{ empty?: boolean; onChange?: (arg0?: string) => void }> = (
  props,
) => {
  /** 当前上传的文件 */
  const [importFile, setImportFile] = useState<{
    status?: 'uploading' | 'done' | 'error';
    percent?: number;
    name?: string;
  }>({});
  
  const handleBeforeUpload = useCallback((file: RcFile): boolean | Upload.LIST_IGNORE => {
    const fileSize = 10;
    const isLtFileSize = file.size / 1024 / 1024 < fileSize;
    /** 文件类型 */
    if (fileTypeList.indexOf(file.type) === -1) {
      message.error(`文件类型不符合`);
      return Upload.LIST_IGNORE;
    }
    /** 文件大小 */
    if (!isLtFileSize) {
      message.error(`单个文件不超过${fileSize}MB`);
      return Upload.LIST_IGNORE;
    }
    return true;
  }, []);


  /**
   * uploading: 是否上传中
   * uploadRes: 上传结果
   */
  const {
    loading: uploading,
    data: uploadRes,
    run: triggerUpload,
  } = useRequest(
    (formData, { onProgress }) =>
     //path根据实际地址填写
      uploadService(formData, { onProgress, path: '/api/xxxxxxxxx.......'}),
    {
      manual: true,
      onSuccess: (res) => {
        setImportFile({ ...importFile, percent: 100, status: 'done' });
      },
      onError: (e) => {
        setImportFile({ ...importFile, status: 'error' });
      },
    },
  );

  const uploadConfig = {
    multiple: false,
    // data: 自定义请求参数 根据实际业务替换
    data: { id:'1234' },
    onStart(file: File) {
      setImportFile({ ...file, ...importFile, name: file?.name, percent: 0, status: 'uploading' });
    },
    onProgress({ percent }: { percent: number }) {
      setImportFile({ ...importFile, percent });
    },
    // 自定义上传请求
    customRequest({
      data,
      file,
      filename,
      onProgress,
    }: {
      onProgress: ({ percent }: { percent: number }, file: File) => void;
      data: { id: string };
      filename: string;
      file: File;
    }) {
      const formData = new FormData();
      if (data) {
        /** 添加额外参数 */
        Object.keys(data).forEach((key) => {
          formData.append(key, data[key]);
        });
      }
      /** 添加文件参数 */
      formData.append(filename, file);
      triggerUpload(formData, { onProgress });
    },
  };

  return (
    <div className={styles.uploader}>
      <Upload.Dragger
        accept={fileTypeList.join(',')}
        beforeUpload={handleBeforeUpload}
        disabled={uploading || !!importFile?.status}
        name="data"
        className={styles.dragger}
        showUploadList={false}
        {...uploadConfig}
      >
        <>
          <p className="ant-upload-drag-icon">
            <ContainerOutlined />
          </p>
          <p className="ant-upload-text">点击或将文件拖拽到这里上传</p>
        </>
      </Upload.Dragger>

      {importFile?.status && (
        <>
          <div>
            <div className={styles.nameItem}>
             <span>{importFile?.name}</span>
              <DeleteOutlined
                style={{ cursor: 'pointer' }}
                onClick={() => {
                  setImportFile({});
                }}
              />
            </div>
          </div>
            <Progress
              showInfo={false}
              style={{ paddingLeft: 15 }}
              percent={importFile?.percent}
              strokeWidth={2}
              status={importFile?.status === 'uploading' ? 'active' : 'exception'}
            />
        </>
      )}
    </div>
  );
};

export default Uploader;

踩坑记录

1.响应拦截不执行

上述代码,可完成正常上传业务需求,但如若基于umi-request 注册了request.interceptors.response,此时并不会执行,因为 umi-request-progress 修改了umiRequestCoreTypeajaxRequest ,而 umi-request 对于 options.umiRequestCoreType为非normal类型时,会跳过interceptor,源码如下:在这里插入图片描述
在这里插入图片描述

2.无法捕获401/404等ResponseError

原因:umi-request-progress resolve出来的是普通的xhr.response,并不是 Response实例,不具备 clone 等方法,但 umi-request 是基于res是否具有 clone 方法的判断得到 copy,方可根据http status,throw 对应的ResponseError,源码如下:
在这里插入图片描述

在这里插入图片描述在这里插入图片描述

解法

  1. 创建自定义中间件,结合 umi-request-progress代码,稍作调整
function ajaxRequest(url, options) {
  return new Promise(function (resolve, reject) {
    var _a, _b, _c;
    const method = (_a = options.method) != null ? _a : 'get';
    const headers = (_b = options.headers) != null ? _b : {};
    const {
      onReqProgress,
      onResProgress,
      credentials,
      responseType,
      body,
      parseResponse,
      timeout,
      cancelToken,
      signal,
    } = options;
    const xhr = new XMLHttpRequest();
    if (timeout != null) {
      xhr.timeout = timeout;
    }
    switch (credentials) {
      case 'include': {
        xhr.withCredentials = true;
        break;
      }
      case 'omit': {
        xhr.withCredentials = false;
        break;
      }
    }
    if (responseType !== 'formData') {
      xhr.responseType =
        (_c = responseType == null ? void 0 : responseType.toLowerCase()) != null ? _c : 'json';
    }
    xhr.open(method, url);
    let headersEntries = [];
    if (Array.isArray(headers)) {
      headersEntries = headers;
    } else if (headers instanceof Headers) {
      headers.forEach((value, key) => {
        headersEntries.push([key, value]);
      });
    } else {
      headersEntries = Object.entries(headers);
    }
    for (const [key, value] of headersEntries) {
      xhr.setRequestHeader(key, value);
    }
    if (cancelToken) {
      cancelToken.promise.then(function () {
        xhr.abort();
      });
    }
    if (signal) {
      signal.addEventListener(
        'abort',
        function () {
          xhr.abort();
        },
        {
          once: true,
        },
      );
    }
    xhr.onload = () => {
      // resolve(parseResponse === false ? xhr.responseText : xhr.response);
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(parseResponse === false ? xhr.responseText : xhr.response);
      } else {
        const myBlob = new Blob();
        const myOptions = {
          status: xhr.status,
          statusText: 'SuperSmashingGreat!',
        };
        //创建 Response实例
        const myResponse = new Response(myBlob, myOptions);
        resolve(myResponse);
      }
    };
    xhr.onerror = reject;
    if (xhr.upload && onReqProgress) {
      xhr.upload.onprogress = onReqProgress;
    }
    if (onResProgress) {
      xhr.onprogress = onResProgress;
    }
    xhr.send(body);
  });
}
const progressMiddleware = async function (ctx, next) {
  const { url, options } = ctx.req;
  if (!(options.onReqProgress || options.onResProgress)) {
    await next();
    return;
  }
  //如需执行响应拦截 不可修改 __umiRequestCoreType__
  // options.__umiRequestCoreType__ = "ajaxRequest";
  const response = await ajaxRequest(url, options);
  ctx.res = response;
  await next();
};
export { progressMiddleware as default };

2.可提issue至 umi-request-progress 仓库

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值