vue-大文件分片及斷點上傳


最近開發過程中,有一個大文件分片上傳的功能,借鑒於網上的思路,結合自己后端的邏輯,完成了這個功能,在此記錄如下:

界面展示:

 

一、將大文件分片上傳寫為一個組件,可以全局注冊該組件,也可以在使用的頁面注冊該組件,使用vuex通訊進行組件間傳值

由於我有好幾個頁面需要使用大文件上傳,所以我是在App.vue注冊該組件

<template>
  <a-config-provider :locale="locale">
    <div id="app">
      <router-view />
      <!-- 將分片上傳組件全局注冊 -->
      <big-uploader></big-uploader>
    </div>
  </a-config-provider>
</template>
<script>
...
import BigUploader from './components/bigUploader.vue';
export default {
  data () {
    return {
      ...
    };
  },
  components: { BigUploader },
  ...
};
</script>
View Code

 

二、文件選擇上傳框通過文件上傳前的鈎子,會有項目的業務邏輯判斷,校驗文件空間是否還有,判斷文件是否為重復文件,提示是覆蓋還是重新上傳,校驗都通過后就去調起大文件上傳的組件

beforeUpload(file) {
      const self = this
      let size = file.size
      let params = {
        fileSize: size,
        projectCode: self.projectCode
      }
      if (size === 0) {
        this.$message.error('上傳文件不能為0KB')
        return false
      }
      const fileName = file.name
      let fileType = fileName.split('.')
      fileType = fileType[fileType.length - 1].toLowerCase()//轉小寫
      if (this.accept.indexOf(fileType) == -1) {
        this.$message.error('請上傳指定文件類型')
        return false;
      }
      if (this.uploadFlag) return false;
      this.uploadFlag = true
      return new Promise(() => {
        try {
          // 一、校驗文件空間
          request(xxx).then(res => {
            if (res.code === 200) {
              // 二、計算MD5
              self.calculate(file, async function (md5) {
                const par = {
                  fileName: file.name,
                  md5Value: md5,
                  parantCode: self.parentCode,
                  projectCode: self.projectCode
                }
                // 三、文件覆蓋檢查
                request(xxx).then(res => {
                  console.log(res)
                  if (res.code === 200 && res.data) {
                    //同名同類型提示是否覆蓋
                    self.$confirm({
                      title: false,
                      content: '該文件夾下存在同名同類型的文件,是否確認覆蓋?',
                      onOk() {
                        // 覆蓋
                        self.implementSetFile(file, md5)
                      },
                      onCancel() {
                        self.handelModelCancal();
                      },
                    });
                  } else {
                    // 新文件
                    self.implementSetFile(file, md5)
                  }
                  return false
                })
              })
            }
          }).catch(() => {
            self.uploadFlag = false
            return false;
          })
        }
        catch {
          self.uploadFlag = false
          return false;
        }
      })
    }, 
implementSetFile(file, md5) {
      // 設置當前上傳文件
      this.setFile({
        page: this.curMenuName.name,
        bucket: 'privately',
        projectCode: this.projectCode,
        parentCode: this.parentCode,
        record: this.record,
        file: file,
        md5: md5
      })
      // 設置顯示上傳框
      this.setShowBigUpload(true)
      // this.$emit('ok');
      this.uploadFlag = false
      this.$emit('closeUpload')
    },
    /**
     * 計算md5,實現斷點續傳及秒傳
     * @param file
     */
    calculate(file, callBack) {
      var fileReader = new FileReader(),
        blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice,
        chunkSize = 2097152,
        // read in chunks of 2MB  
        chunks = Math.ceil(file.size / chunkSize),
        currentChunk = 0,
        spark = new SparkMD5();

      fileReader.onload = function (e) {
        spark.appendBinary(e.target.result); // append binary string  
        currentChunk++;

        if (currentChunk < chunks) {
          loadNext();
        }
        else {
          callBack(spark.end());
        }
      };

      function loadNext() {
        var start = currentChunk * chunkSize,
          end = start + chunkSize >= file.size ? file.size : start + chunkSize;

        fileReader.readAsBinaryString(blobSlice.call(file, start, end));
      }

      loadNext();
    }, 
                    
View Code

三、大文件組件

data中定義當前上傳下標,上傳並發數,允許每個分片最多允許重傳數,上傳文件列隊數組等值

通過mapGetters獲取vuex值

...mapGetters({
      showUploadBox: `${NAMESPACE_UPLOAD}/${GET_SHOW_BIG_UPLOAD}`,
      file: `${NAMESPACE_UPLOAD}/${GET_FILE}`,
      chunkSize: `${NAMESPACE_UPLOAD}/${GET_CHUNKSIZE}`,
      threads: `${NAMESPACE_UPLOAD}/${GET_THREADS}`,
      getUplodRes: `${NAMESPACE_UPLOAD}/${GET_UPLOAD_RES}`,
    }),

 

通過監聽file變化,觸發大文件上傳排隊

watch: {
    file: {
      immediate: true,
      handler: function (newVal) {
        if (newVal) {
          const tempMd5 = newVal.md5
          // 判斷文件是否在排隊中
          if (this.upFileObj[tempMd5]) {
            this.$message.warn('當前文件已經在上傳排隊中,請不要重復選擇')
          } else {
            newVal.fileIndex = this.uploadFiles.length
            this.upFileObj[tempMd5] = newVal
            this.uploadQueue(tempMd5)

          }

        }
      }
    }
  }
View Code

 

通過uploadQueue組件上傳排隊當前文件的分片,通過md5校驗資源是否已經存在,記錄資源是否存在和是否需要上傳

/*
    * 上傳排隊
    * 計算當前文件的Md5和分片
    */
    uploadQueue(md5) {
      console.log('1.當前文件上傳排隊')
      const self = this;
      const tempFile = self.upFileObj[md5]
      const fileName = tempFile.file.name
      const curFileSize = tempFile.file.size
      self.handleInitRawFile(tempFile);
      // 檢驗資源是否存在
      request(xxx).then(res => {
        if (res.code === 200) {
          var resData = res.data
          const fileIndex = tempFile.fileIndex
          const filesArr = self.uploadFiles;
          if (resData) {
            // 資源已經存在
            console.log('3.上傳文件已經存在', resData)
            filesArr[fileIndex].Already = true
            filesArr[fileIndex].resData = resData
            self.$set(filesArr, fileIndex, filesArr[fileIndex]);
          } else {
            // 需要上傳
            let needChunk = false
            // 不需要分片
            if (curFileSize < self.chunkSize) {
              needChunk = false
            } else {
              needChunk = true
            }
            filesArr[fileIndex].needChunk = needChunk
            self.$set(filesArr, fileIndex, filesArr[fileIndex]);
          }
          // 判斷是否正在上傳中
          if (self.uploadLock) {
            console.log('有文件正在上傳中...')
          } else {
            self.beforeUpload()
          }
        }
      })
    },
    /*
    ** 初始化部分自定義上傳屬性
    */
    handleInitRawFile(rawFile) {
      console.log('2.初始化部分自定義上傳屬性')
      rawFile.status = fileStatus.md5;
      rawFile.initFail = this.file
      rawFile.chunkList = [];
      rawFile.uploadProgress = 0;
      rawFile.fakeUploadProgress = 0; // 假進度條,處理恢復上傳后,進度條后移的問題
      rawFile.hashProgress = 0;
      this.uploadFiles.push(rawFile);
    },
View Code

 

文件正式上傳前,判斷資源是否存在直接秒傳,是否需要上傳,上傳的話是否需要進行分片,秒傳和不需要分片都是簡單的業務邏輯,下面展示一下主要的分片代碼

需要分片,組件分片上傳數組

/*
    ** 開始組建上傳數組
    */
    async handleUpload() {
      console.log('6.開始組建上傳數組')
      if (!this.uploadLock) return;
      const filesArr = this.uploadFiles;
      const fileUpIdx = this.fileUpIdx
      const fileChunkList = this.createFileChunk(filesArr[fileUpIdx].file);
      if (filesArr[fileUpIdx].status !== 'resume') {
        this.status = Status.hash;
        // hash校驗,是否為秒傳
        filesArr[fileUpIdx].hash = await this.calculateHash(fileChunkList);
        // 若清空或者狀態為等待,則跳出循環
        if (this.status === Status.wait) {
          console.log('若清空或者狀態為等待,則跳出循環');
          return
        }
      }

      this.status = Status.uploading;
      filesArr[fileUpIdx].status = fileStatus.uploading;
      filesArr[fileUpIdx].fileHash = filesArr[fileUpIdx].hash; // 文件的hash,合並時使用
      filesArr[fileUpIdx].chunkList = fileChunkList.map(({ file }, index) => ({
        fileHash: filesArr[fileUpIdx].hash,
        fileName: filesArr[fileUpIdx].file.name,
        index,
        hash: filesArr[fileUpIdx].hash + '-' + index,
        chunk: file,
        size: file.size,
        uploaded: false, // 標識:是否已完成上傳
        progress: 0,
        status: 'wait' // 上傳狀態,用作進度狀態顯示
      }));
      this.$set(filesArr, fileUpIdx, filesArr[fileUpIdx]);
      ...
    },
View Code
分片-初始化任務的時候,將分片數組及文件md5傳給后端,后端返回該文件上傳的任務id,及沒有上傳的片段數組及小片段id,已經上傳的片段,實現秒傳效果,這里就是斷點續傳的主要邏輯思路
initJob() {
      console.log('7.分片-初始化任務')
      const filesArr = this.uploadFiles;
      const fileUpIdx = this.fileUpIdx
      const uploadFileMd5 = filesArr[fileUpIdx].md5
      let detailList = []
      filesArr[fileUpIdx].chunkList.forEach((item, idx) => {
        detailList.push({
          extInfo: '',
          // file: item.chunk,
          num: idx
        })
      })
      const params = {
        md5HashValue: uploadFileMd5,
        detailList: detailList
      }
      request(xxx).then(res => {
        console.log('分片-初始化任務', res)
        if (res.code === 200) {
          const resData = res.data
          if (resData.jobStatus === 1) {
            console.log('上傳文件已經存在', resData)
          } else {
            let sliceList = []
            this.jobCode = resData.jobCode
            resData.sliceList.forEach(item => {
              const tt = {
                ...item,
                ...filesArr[fileUpIdx].chunkList[item.num]
              }
              sliceList.push(tt)
            })
            // this.sliceList = sliceList
            filesArr[fileUpIdx].chunkList = sliceList
            this.$set(filesArr, fileUpIdx, filesArr[fileUpIdx]);
            //沒有上傳的分片
            let noUpSlice = [], yesUpSlice = [];
            sliceList.filter((item, idx) => {
              item.num = idx
              if (item.uploadStatus === 0) {
                noUpSlice.push({ ...item });
              } else {
                yesUpSlice.push({ ...item });
              }
            })
            // const noUpSlice = sliceList.filter(({ uploadStatus }) => uploadStatus === 0)
            console.log('沒有上傳的分片', noUpSlice)
            if (noUpSlice.length === 0) {
              this.mergeRequest();
            } else {
              if (yesUpSlice.length > 0) {
                yesUpSlice.forEach(item => {
                  this.createProgresshandler(100, item.num)
                })
              }
              this.uploadChunks(noUpSlice, sliceList);
            }
          }
        }
      })
    },
View Code
將切片傳輸給服務端,進行並發上傳處理,所有的分片上傳完成,向服務端進行合並請求
async uploadChunks(data, allData) {
      console.log('8.將切片傳輸給服務端')
      return new Promise(async (resolve, reject) => {
        const requestDataList = data.map(({ sliceCode, num, chunk }) => {
          const formData = new FormData();
          formData.append('sliceCode', sliceCode);
          formData.append('file', chunk);
          return { formData, num };
        })
        try {
          const ret = this.sendRequest(requestDataList, data, allData);
        } catch (error) {
          // 上傳有被reject的
          this.$message.error('親 上傳失敗了,考慮重試下呦' + error);
          return;
        }
        // 合並切片
        const isUpload = data.some((item) => item.uploadStatus === 0);
        if (!isUpload) {
          // 執行合並
          try {
            await this.mergeRequest();
            resolve();
          } catch (error) {
            reject();
          }
        }
      });
    },
sendRequest(forms, chunkData, allData) {
      console.log('9.分片-並發上傳處理')
      var finished = 0;
      const total = forms.length;
      const that = this;
      const retryArr = []; // 數組存儲每個文件hash請求的重試次數,做累加 比如[1,0,2],就是第0個文件切片報錯1次,第2個報錯2次
      // const md5Hash = allData[0].fileHash
      const filesArr = this.uploadFiles;
      const fileUpIdx = this.fileUpIdx
      const md5Hash = filesArr[fileUpIdx].md5

      return new Promise((resolve, reject) => {
        const handler = () => {
          if (forms.length) {
            // 出棧
            const formInfo = forms.shift();
            const formData = formInfo.formData;
            const index = formInfo.num;
            /*
            ***  開始分片上傳
            */
            // console.log('當前分片上傳', allData[index])
            if (allData[index]) {
              request(xxx)
                .then(res => {
                  // 更改狀態
                  allData[index].uploaded = true;
                  allData[index].uploadStatus = 1;
                  allData[index].status = 'success';
                  finished++;
                  if (finished === chunkData.length) {
                    // 執行合並
                    this.mergeRequest();
                  }
                  handler();
                }).catch((e) => {
                  // 若狀態為暫停或等待,則禁止重試
                  if ([Status.pause, Status.wait].includes(this.status)) return;
                  console.warn('出現錯誤', e);
                  console.log('當前分片上傳報錯', retryArr);
                  if (typeof retryArr[index] !== 'number') {
                    retryArr[index] = 0;
                  }
                  // 更新狀態
                  allData[index].status = 'warning';
                  // 累加錯誤次數
                  retryArr[index]++;
                  // 重試3次
                  if (retryArr[index] >= this.chunkRetry) {
                    console.warn(' 重試失敗--- > handler -> retryArr', retryArr, allData[index].hash);
                    return reject('重試失敗', retryArr);
                  }
                  // console.log('handler -> retryArr[finished]', `${allData[index].hash}--進行第 ${retryArr[index]} '次重試'`);
                  // console.log(retryArr);

                  this.tempThreads++; // 釋放當前占用的通道
                  // 將失敗的重新加入隊列
                  forms.push(formInfo);
                  handler();
                })
            }
          }
          if (finished >= total) {
            resolve('done');
          }
        };
        // 控制並發
        for (let i = 0; i < this.tempThreads; i++) {
          handler();
        }
      });
    },
View Code

 通知服務端合並切片,設置總的文件進度,並設置上傳結果,回傳給頁面展示新增文件,進行下一步業務操作

在此大文件上傳的整個思路就完成了。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM