ant-design-vue 文件分块、控制并发、重试上传

126 阅读1分钟
<template>
  <div>
    <a-upload
      :accept="accept"
      :beforeUpload="beforeUpload"
      :disabled="disabled"
      :fileList="fileList"
      :list-type="listType"
      :multiple="true"
      :remove="handleImageRemove"
      @change="handleChange"
    >
      <a-button :disabled="disabled">
        <a-icon type="upload" />
        {{ text }}
      </a-button>
    </a-upload>
  </div>
</template>
<script>
import md5 from "js-md5";
import {
  getCheckFile,
  getChunkFile,
  getUploadPart,
  getMergeFile,
  getCheckCosFile,
  getChunkCosFile,
  getCosUploadPart,
  getMergeCosFile
} from "@/api/common";
export default {
  name: "ChuckUploadComponent",
  props: {
    isCosType: { type: Boolean, required: false, default: false },
    disabled: { type: Boolean, required: false, default: false },
    count: { type: Number, required: false, default: 1 },
    uploadLists: {
      type: Array,
      required: false,
      default: () => {
        return [];
      }
    },
    listType: { type: String, required: false, default: "text" },
    text: { type: String, required: false, default: "选择文件" },
    accept: {
      type: String,
      required: false,
      default: "*"
    },
    ext: {
      type: Array,
      required: false,
      default: () => {
        return []; // ['image/jpeg', 'image/jpg'];
      }
    }
  },
  data() {
    return {
      fileList: [],
      tempThreads: 5,
      chunkRetry: 4,
      chunkSize: 5 * 1024 * 1024
    };
  },
  created() {},
  watch: {
    uploadLists(value, oldValue) {
      this.fileList = value;
    }
  },
  methods: {
    beforeUpload(file) {
      this.fileList = [...this.fileList, file];
      file.status = "uploading";
      this.checkFile(file);
      return false;
    },
    // 判断文件是否上传过,获取fileId
    checkFile(file) {
      const md5 = this.getFileMD5(file);
      file.md5 = md5;
      const formData = new FormData();
      formData.append("fileMd5", md5);
      formData.append("fileName", file.name);
      if (this.isCosType) {
        formData.append("fileSize", file.size);
      }
      const http = this.isCosType
        ? getCheckCosFile(formData)
        : getCheckFile(formData);
      http.then(async res => {
        if (res.code === 0) {
          if (res.data.status === 1) {
            // 文件存在
            if (res.data.fileId) {
              file.fileId = res.data.fileId;
              this.fileList.forEach(item => {
                if (item.md5 === md5) {
                  item.status = "done";
                  this.$emit("fileId", res.data.fileId);
                  this.$message.success(item.name + " 文件上传成功!");
                }
              });
            } else {
              // console.log(`fileId不存在`)
              this.$message.error(res.msg);
            }
          } else {
            // 文件不存在或不完整,发送该文件
            await this.uploadChunks(file);
          }
        }
      });
    },
    // 根据文件大小,分配上传分片大小
    updateCchunkSize(file) {
      if (file.size > 2000 * 1024 * 1024) {
        this.chunkSize = 1024 * 1024 * 15;
      } else if (file.size > 1000 * 1024 * 1024) {
        this.chunkSize = 1024 * 1024 * 10;
      } else if (file.size > 500 * 1024 * 1024) {
        this.chunkSize = 1024 * 1024 * 8;
      } else {
        this.chunkSize = 2 * 1024 * 1024;
      }
    },
    createFileChunk(file, size) {
      const fileChunkList = [];
      let count = 0;
      while (count < file.size) {
        fileChunkList.push({
          file: file.slice(count, count + size)
        });
        count += size;
      }
      return fileChunkList;
    },
    async uploadChunks(file) {
      this.updateCchunkSize(file);
      const fileChunkList = this.createFileChunk(file, this.chunkSize);
      file.chunkList = fileChunkList.map(({ file }, index) => ({
        index,
        source: file,
        size: file.size
      }));
      var chunkData = file.chunkList;
      return new Promise((resolve, reject) => {
        const requestDataList = chunkData.map(value => {
          const formData = new FormData();
          formData.append("fileMd5", file.md5);
          formData.append("chunk", value.index);
          formData.append("file", value.source);
          return { formData, index: value.index, md5: file.md5, file };
        });
        try {
          const ret = this.sendRequest(requestDataList);
          // console.log('上传结束,参数:', ret, chunkData, file.md5)
          resolve(ret);
        } catch (error) {
          this.$message.error("上传失败,请重试");
          reject("sendRequest 失败", error);
        }
      }).then(async res => {
        if (res == file.md5) {
          // console.log('--- ' + file.name + ' 文件开始合并----')
          await this.mergeRequest(file.md5);
        }
      });
    },
    // 并发,重试请求
    async sendRequest(list) {
      var finished = 0;
      const retryArr = []; // retryArr.length代表请求数,值代表重试次数
      var currentFileInfo;
      const total = list.length;
      // 所有请求都存放这个promise中
      return new Promise((resolve, reject) => {
        const handler = () => {
          if (list.length) {
            const formInfo = list.shift();
            const index = formInfo.index;
            const http = this.isCosType
              ? getChunkCosFile(formInfo.formData)
              : getChunkFile(formInfo.formData);
            http
              .then(res => {
                // if (res.code === 0) {
                //   //分片存在,跳过. 待测试
                //   finished++;
                //   handler();
                // } else {
                const http = this.isCosType
                  ? getCosUploadPart(formInfo.formData)
                  : getUploadPart(formInfo.formData);
                // 分块不存在或不完整,重新发送该分块内容
                http
                  .then(res => {
                    if (res.code === 0) {
                      // const date = new Date();
                      // console.log(formInfo.file.name + 'UploadPart', formInfo, date.getTime(), res)
                      currentFileInfo = formInfo;
                    }
                    return res;
                  })
                  .then(res => {
                    if (res.code === 0) {
                      finished++;
                      handler();
                    } else {
                      this.$message.error(res.msg);
                    }
                  })
                  .catch(e => {
                    if (typeof retryArr[index] !== "number") {
                      retryArr[index] = 1;
                    }
                    if (retryArr[index] >= this.chunkRetry) {
                      return reject("重试失败", retryArr);
                    }
                    // console.log(`${formInfo.file.name}--文件的第 ${index} 个分块,开始进行第 ${retryArr[index]} 次重试`);
                    retryArr[index]++; // 累加
                    this.tempThreads++; // 释放当前占用的通道
                    list.push(formInfo); // 将失败的重新加入队列
                    handler();
                  });
                // }
              })
              .catch(e => {
                this.$message.error("上传失败,请重试");
              });
          }
          if (finished >= total) {
            resolve(currentFileInfo.md5); // 输出当前完成上传的文件信息
          }
        };
        // 控制并发
        for (let i = 0; i < this.tempThreads; i++) {
          handler();
        }
      });
    },
    async mergeRequest(fileMd5) {
      return new Promise((resolve, reject) => {
        const mergeFormData = new FormData();
        mergeFormData.append("fileMd5", fileMd5);
        const http = this.isCosType
          ? getMergeCosFile(mergeFormData)
          : getMergeFile(mergeFormData);
        http
          .then(res => {
            if (res.code === 0) {
              this.fileList.forEach(item => {
                if (item.md5 === fileMd5) {
                  item.status = "done";
                  item.fileId = res.data.fileId;
                  this.$emit("fileId", item.fileId);
                  this.$message.success(item.name + " 文件上传成功!");
                  // console.log(item.name + ' 文件上传成功!')
                  resolve("sucess");
                }
              });
            } else {
              this.$message.error(res.msg);
            }
          })
          .catch(err => {
            // console.log('mergeRequest -> err', err);
            reject("合并失败", err);
          });
      });
    },
    getFileMD5(file) {
      return md5(file.name + file.size + file.lastModifiedDate);
      // return md5(file.name + file.size + file.lastModifiedDate + Math.random(1000) + new Date().getTime());
      // const reader = new FileReader();
      // reader.onload = function (e) {
      //   file.fileMD5 = md5(e.target.result);
      //   console.log(e.target.result);
      // }
      // reader.readAsText(file);
    },
    handleChange(info) {
      this.$set(this, "fileList", info.fileList);
      const status = info.file.status;
      if (status === "done") {
        this.$message.success("上传成功");
      } else if (status === "uploading") {
        // this.$message.info(`${info.file.name} 上传中...`);
      } else if (status === "error") {
        this.$message.error(`${info.file.name} 上传失败`);
      }
    },
    handleImageRemove(res) {
      this.fileList.forEach((item, idx) => {
        if (item.uid === res.uid) {
          this.fileList.splice(idx, 1);
          this.$message.success("删除成功");
        }
      });
      this.$emit("getFileList", this.fileList);
    }
  }
};
</script>
<style>
/* 隐藏预览功能 */
span.ant-upload-list-item-actions a {
  display: none;
}
</style>