分片上传简单实现重点是Promise的运用

1.钩子方法

import _ from "lodash";
import React, { useEffect } from "react";
import axios from "axios";

// mock上传接口
const uploadMockPromise = ({ formData, i }) => {
  return new Promise((resolve, reject) => {
    axios
      .post("/upload/" + i, formData)
      .then((res) => {
        return resolve({ KEY: "key-01", ID: "id-01" });
      })
      .catch((err) => {
        // if (i === 3|| i === 4||i === 5) {
        //   return reject({ err: true }); // 需要测试续传的时候需要返回错误数据
        // }
        return resolve({ KEY: "key-01", ID: "id-01" });
      });
  });
};

// mock合并接口
const mockMergePromise = ({ uid, option }) => {
  return new Promise((resolve) => {
    axios
      .post("/merge", { uid })
      .then((res) => {
        console.log(res, "merge 成功");
        return resolve({ success: true });
      })
      .catch((err) => {
        console.log("merge 失败", err);
        return resolve({ success: true });
      });
  });
};

const useUpload = ({ getFileList, getUploadDataArr }) => {
  const uploadObjectRef = React.useRef<any>({ current: { fileList: [] } });

  useEffect(() => {
    getUploadDataArr(uploadObjectRef.current.uploadDataArr);
  }, [uploadObjectRef.current.uploadDataArr]);

  const getFilePara = (option: any) => {
    const fileSize = option.file.size;
    let everyPieceSize: number = 1024 * 1024;
    const fileSlitLen = Math.ceil(fileSize / everyPieceSize);
    console.log(everyPieceSize, fileSlitLen);
    return {
      fileSlitLen,
      everyPieceSize,
    };
  };

  const splitFile = (file: Blob, pieceByte: number, index: number) => {
    const chunkSum = Math.ceil(_.get(file, "size") / pieceByte); // 切片总数
    const chunkSize = Math.ceil(_.get(file, "size") / chunkSum); // 除以切片总数求得平均每片size多大
    const { size, type } = file;
    const filePrototype = File.prototype;
    let blobSlice =
      filePrototype.slice ||
      _.get(filePrototype, "mozSlice") ||
      _.get(filePrototype, "webkitSlice");
    if (size > pieceByte) {
      let start = index * chunkSize; // 切片开始位置
      let end = start + chunkSize; // 切片结束位置
      console.log(start, end, "start---end");
      const file1 = _.get(file, "originFileObj");
      let item: any = null;
      try {
        item = blobSlice.call(file, start, end, type); // 切割的文件
      } catch (error) {
        // 如果file没有slice方法, 就从file.originFileObj找
        item = blobSlice.call(file1, start, end, type); // 切割的文件
      }

      return item;
    }
    return file;
  };

  const getFormData = ({ fileBlob }: any, option: any) => {
    const formData = new FormData();
    formData.append("FILE", fileBlob, option.file.name);
    return formData;
  };

  /**
   * 代码的核心思路为:
  
  1.先初始化 promiseListLimit 个 promise 实例,将它们放到 executing 数组中
  2.使用 Promise.race 等待这 promiseListLimit 个 promise 实例的执行结果
  3.一旦某一个 promise 的状态发生变更,就将其从 executing 中删除,然后再执行循环生成新的 promise,放入executing 中
  4.重复2、3两个步骤,直到所有的 promise 都被执行完
  5.最后使用 Promise.all 返回所有 promise 实例的执行结果
   */
  const concurrencyPromisePool = async (
    promiseListLimit,
    promiseArr,
    mockTimeoutDataFn
  ) => {
    const allPromiseArr: any[] = []; // 用于存放所有的promise实例
    const executing: any[] = []; // 用于存放目前正在执行的promise
    for (let index = 0; index < promiseArr.length; index++) {
      const promise = promiseArr[index];
      const mockPromise: Promise<any> = mockTimeoutDataFn
        ? mockTimeoutDataFn(promise, index)
        : promise; // 回调函数返回的必须是promise,否则需要使用Promise.resolve进行包裹
      allPromiseArr.push(mockPromise);
      if (promiseListLimit <= promiseArr.length) {
        // then回调中,当这个promise状态变为fulfilled后,将其从正在执行的promise列表executing中删除
        const executingItem: Promise<any> = mockPromise.then(() => {
          const idx = executing.indexOf(executingItem);
          return executing.splice(idx, 1); // splice: 如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组。
        });
        executing.push(executingItem);
        console.log(`并发接口数量: ${executing.length}`);
        if (executing.length >= promiseListLimit) {
          // 一旦正在执行的promise列表数量等于限制数,就使用Promise.race等待某一个promise状态发生变更,
          // 状态变更后,就会执行上面then的回调,将该promise从executing中删除,
          // 然后再进入到下一次for循环,生成新的promise进行补充
          await Promise.race(executing);
        }
      }
    }
    return Promise.all(allPromiseArr);
  };

  // mock promise, 方便查看并发的效果
  const resovePromiseFn = (promiseFn, i) => {
    console.log("开始--", i);
    return new Promise((resolve) => {
      console.log("结束--", i);
      return resolve(promiseFn());
    });
  };

  /**
   * 新增上传--当第一次成功后执行剩下的并发方法
   * @param option 文件
   * @param uploadDataArr 分片结果数据:包括成功和失败的
   */
  function runFn({ option }: { option: any }, uploadDataArr: any[]) {
    const step = 5; // 并发请求五条接口
    let fnArr: any[] = [];
    const { fileSlitLen, everyPieceSize } = getFilePara(option);
    const fileBlob: any = splitFile(option.file, everyPieceSize, 0);
    const formData = getFormData({ fileBlob }, option);
    let key = null;
    let uid = null;
    uploadMockPromise({
      i: 0,
      formData,
    }).then((res) => {
      key = _.get(res, "KEY");
      uid = _.get(res, "ID");
      uploadDataArr[0] = {
        KEY: _.get(res, "KEY"),
        ID: _.get(res, "ID"),
        PART_NUM: 1,
        fileBlob,
        successFLg: true,
      };
      go();
      return res;
    });

    function go() {
      let isError = false;
      for (let i = 1; i < fileSlitLen; i++) {
        const partNumber = i + 1;
        const fileBlob: any = splitFile(option.file, everyPieceSize, i);
        const formData = getFormData({ fileBlob }, option);
        const f1 = () => {
          return uploadMockPromise({
            i,
            formData,
          })
            .then((res) => {
              uploadDataArr[i] = {
                KEY: _.get(res, "KEY"),
                ID: _.get(res, "ID"),
                PART_NUM: partNumber,
                fileBlob,
                successFLg: true,
              };
              return res;
            })
            .catch((err) => {
              uploadDataArr[i] = {
                option, // 为了续传时获取文件信息
                KEY: key,
                ID: uid,
                PART_NUM: partNumber,
                fileBlob,
                successFLg: false,
              };
              isError = true;
              return err;
            })
            .finally(() => {
              console.log(uploadDataArr, "uploadDataArr");
              console.log("finally");
            });
        };
        fnArr.push(f1);
      }

      concurrencyPromisePool(step, fnArr, resovePromiseFn)
        .then((res) => {
          console.log(res, "concurrencyPromisePool", fileSlitLen);
          uid = _.get(res, "uid");
          // 如果有上传未成功的切片, 就不让合并
          if (isError) return;
          return mockMergePromise({ uid, option })
            .then((res) => {
              console.log("mockMergePromise--success");
              const fileList = _.get(uploadObjectRef.current, "fileList") || [];
              const fileName = _.get(option, "file.name")
              uploadObjectRef.current.fileList = [
                ...fileList,
                { name: fileName },
              ];
              getFileList(uploadObjectRef.current.fileList);
              uploadObjectRef.current.uploadDataArr = [];
            })
            .catch((err) => {
              console.log("mockMergePromise--errors");
            });
        })
        .catch((err) => {
          console.log(err);
        });
    }
  }

  const uploadFn = async({ option, index }) => {
    const arr0 = _.get(option, 'fileList');
    await fn({ file: _.get(arr0, [index]) });
    function fn(option) {
        // 想办法控制必须一个文件成功之后再开始上传下一个文件, 否则uploadObjectRef.current.uploadDataArr这个一维数组就不够用了!!!
      uploadObjectRef.current.uploadDataArr = [];
      return runFn({ option }, uploadObjectRef.current.uploadDataArr);
    }
  };

  /**
   * 新增上传--当第一次成功后执行剩下的并发方法
   * @param option 文件
   * @param uploadDataArr 分片结果数据:包括成功和失败的
   */
  function continueFn({ option }: { option: any }, uploadDataArr: any[]) {
    // 筛选出没有成功上传的数据
    const errArr = _.filter(uploadDataArr, (ee) => !_.get(ee, "successFLg"));
    const step = 5; // 并发请求五条接口
    let fnArr: any[] = [];
    let key = _.get(errArr, "[0].KEY");
    let uid = _.get(errArr, "[0].ID");
    go();
    function go() {
      let isError = false;
      for (let i = 0; i < errArr.length; i++) {
        const fileItem = errArr[i];
        const partNumber = _.get(fileItem, "PART_NUM");
        const index = partNumber - 1;
        const fileBlob: any = _.get(fileItem, "fileBlob");
        const formData = getFormData({ fileBlob }, option);
        const f1 = () => {
          return uploadMockPromise({
            i: index,
            formData,
          })
            .then((res) => {
              uploadDataArr[index] = {
                KEY: _.get(res, "KEY"),
                ID: _.get(res, "ID"),
                PART_NUM: partNumber,
                successFLg: true,
              };
              return res;
            })
            .catch((err) => {
              uploadDataArr[index] = {
                KEY: key,
                ID: uid,
                PART_NUM: partNumber,
                successFLg: false,
              };
              isError = true;
              return err;
            })
            .finally(() => {
              console.log(uploadDataArr, "uploadDataArr");
              console.log("finally");
            });
        };
        fnArr.push(f1);
      }

      concurrencyPromisePool(step, fnArr, resovePromiseFn)
        .then((res) => {
          console.log(res, "concurrencyPromisePool");
          uid = _.get(res, "uid");
          // 如果有上传未成功的切片, 就不让合并
          if (isError) return;
          return mockMergePromise({ uid, option }).then((res) => {
            const fileList = _.get(uploadObjectRef.current, "fileList") || [];
            const fileName = _.get(option, "file.name");
            uploadObjectRef.current.fileList = [
              ...fileList,
              { name: fileName },
            ];
            getFileList(uploadObjectRef.current.fileList);
            uploadObjectRef.current.uploadDataArr = [];
          });
        })
        .catch((err) => {
          console.log(err);
        });
    }
  }

  const continueUploadFn = async({ option }) => {
    await continueFn({ option }, uploadObjectRef.current.uploadDataArr);
  };

  return {
    uploadFn,
    continueUploadFn,
  };
};

export default useUpload;

2. 组件使用:

import React, { useState, useEffect } from "react";
import _ from "lodash";
import { UploadOutlined } from "@ant-design/icons";
import type { UploadProps } from "antd";
import { Button, message, Upload } from "antd";
import useUpload from "./use/useUpload";

const UploadBtn: React.FC = () => {
  const [fileList, setFileList] = useState([]);
  const getFileList = arr =>{
    setFileList(arr)
  }
  const { uploadFn, continueUploadFn } = useUpload({getFileList});
  
  let [option, setOption] = useState({});
  const props: UploadProps = {
    name: "file",
    // action: 'https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188',
    headers: {
      authorization: "authorization-text",
    },
    beforeUpload(file, fileList) {
      console.log(file, fileList);
      return true;
    },
    onChange(info) {
      option = info;
      console.log(info, 333);
      if (info.file.status !== "uploading") {
        console.log(info.file, info.fileList);
      }
      if (info.file.status === "done") {
        message.success(`${info.file.name} file uploaded successfully`);
      } else if (info.file.status === "error") {
        message.error(`${info.file.name} file upload failed.`);
      }
    },
    customRequest: () => {
      console.log(option);
      return uploadFn({ option });
    },
  };

  return (
    <div>
      <Upload
        beforeUpload={props.beforeUpload}
        customRequest={props.customRequest}
        onChange={props.onChange}
        showUploadList={false}
      >
        <Button icon={<UploadOutlined />}>Click to Upload</Button>
      </Upload>
      <ul>
        {_.map(fileList, (item) => (
          <li>{_.get(item, "name")}</li>
        ))}
      </ul>
      <p>{JSON.stringify(fileList)}</p>
      <button
        onClick={() => {
          continueUploadFn({ option });
        }}
      >
        续传
      </button>
    </div>
  );
};

export default UploadBtn;

2.新增上传

1) 第一个切片上传必须单独处理, 因为需要获取到该文件的唯一标识后再执行后续切片的上传

2) 从第二个开始并发上传, 同时允许最多5个接口并行

3) 支持多个文件上传

3.续传

点击相应的未完成续传的数据旁边的续传按钮, 进行续传操作

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值