大文件上传

241 阅读2分钟

突然被问到大文件如何上传,脑海中并么有完整的解决思路, 简单整理下

理论

  • 将需要上传的文件转化为blob流的格式
  • 利用slice分割二进制流,将流分割成相同大小
  • 使用formData包装参数,并行/串行发送请求,
  • 待所有情求发送完毕,给后端发送合并信号进行文件合并

实践

使用antd的upload 组件进行模拟.

<Upload beforeUpload={beforeUpload}>
  <Button icon={<UploadOutlined />}>Upload</Button>
</Upload>

在获取到文件流之后进行等量分割得到由文件流组成的数组

const sizeInfo = 1024 * 1000;

const beforeUpload = (info) => {
    const createFileChunk = (data, size = sizeInfo) => {
        const fileChunkList = [];
        let cur = 0;
        while (cur < data.size) {
          fileChunkList.push({ file: data.slice(cur, cur + size) });
          cur += size;
        }
        return fileChunkList;
      };

    const fileChunkList = createFileChunk(info);
    return false;
  };

接下来拼接请求数据上传文件

// 模拟的接口
const mockQuery = (data, item) => {
    return new Promise((resolve, reject) => {
      axios({
        method: "post",
        url: "xxxxxxxxxxxxxxxxxx",
        data: data,
        headers: {
          "Content-Type": "multipart/form-data",
        },
      })
        .then((res) => {
          resolve(item.index);
        })
        .catch((err) => {
          reject(item.index);
        });
    });
};
const beforeUpload = (info) => {
    const fileChunkList = createFileChunk(info);
    const data = fileChunkList.map((item, index) => {
      return {
        file: item?.file,
        index: `${info?.name}-${index}`,  // 保存下标并追踪记录
      };
    });

    const queryList = data.map((item) => {
      const formData = new FormData();
      formData.append("file", item?.file);
      formData.append("index", item?.index);
      formData.append("filename", this.container.file.name);
      return mockQuery(formData, item);
    });

    Promise.all(queryList).then((res) => {
      console.log(res);
    }).catch((err) => {
      console.log(err)
    });
    return false;
  };

诸多问题

文件秒传

文件秒传是指在文件上传时服务端已经存在了上传的资源,所以在文件再次上传时提示上传成功。 实现文件秒传需要对上传的不同文件生成唯一的hash名称,但是对于大文件计算hash比较浪费性能,建议在web-worker中进行计算。后端如果找到相同的hash文件则直接返回成功信息。

并发控制

如果分割的文件流过多,可能会发出很多的情求,所以进行并发数量的控制是很有必要的。

const sendQuery = (list, max = 6) => {
  const arr = list;
  const newList = list.splice(0, max - 1);
  Promise.all(newList)
    .then((res) => {
      if (arr.length !== 0) {
        sendQuery(arr, max);
      }
    })
    .catch((err) => {
      console.log(err);
    });
};
// queryList 上面请求数组
sendQuery(queryList);

断点续传与报错补传

断点续传

如果因为特殊原因刷新页面或者网络情求出错,我们不需要将已上传的部分再次上传,而是从断开的那个流开始继续上传,这就需要我们在接口返回成功的地方进行处理,记录以及上传的流并保存下来然后从中断的那部分开始上传。

报错补传

假设在上传过程中某一个片段上传失败,我们不需要将所有重新上传,我们只需要将失败的片段重新上传即可,所以我们在接口返回结果中进行处理,记录下失败的片段,并在前面的所有片段请求完成后进行上传。

const sendQuery = (list, max = 6) => {
      const arr = list;
      const newList = list.splice(0, max - 1);
      Promise.all(newList)
        .then((res) => {
          // 在这里拿到成功的index并储存在sessionStory
          // 对于成功的片段在queryList里删除掉,如果上传失败则继续存放在queryList里面继续上传。
          // spliceData -删除queryList中上传成功的情求方法。
          const surplusData = spliceData(res, queryList);
          if (arr.length !== 0) {
            sendQuery(surplusData, max);
          }
        })
        .catch((err) => {
          console.log(err);
        });
    };