当前位置:   article > 正文

前端实现大文件的分片上传、断点续传、秒传的功能 vue2版本_vue2oss上传断点续传分片上传

vue2oss上传断点续传分片上传

现需要做大于1G文件的上传于是就考虑需要切片及断点续传等功能,需和后端友好配合。。。

直接上代码!!!!

技术栈实现:vue2 element minio springboot

  <div v-show="uploadLoading" style="width: 100%">
      <div class="progress-block">
        <el-progress
          :text-inside="true"
          :stroke-width="16"
          :percentage="percentage"
        ></el-progress>
      </div>
    </div>

    <el-upload
      :action="uploadUrl"
      :accept="accept"
      :limit="limit"
      :on-exceed="handleExceed"
      :before-upload="beforeUpload"
      :headers="headers"
      :show-file-list="false"
      :http-request="customHttpRequest"  //自定义上传方法
      :file-list="fileList"
      class="upload-demo"
      :multiple="multiple"
      ref="batchUpload"
      :disabled="disabled"
    />
     <slot name="filePreviewComponent"></slot>
    <draggable
      class="upload-list"
      v-model="fileArr"
      filter=".forbid"
      animation="300"
      @end="onMoveEnd"
    >
       <!-- 自定义的附件列表 -->
      <transition-group>
        <div
          v-for="(item, index) in fileArr"
          :key="index"
          class="upload-list-item"
        >
          <div :title="item.originalName" class="upload-list-item-title">
            <img
              style="margin-right: 5px"
              :src="showTypeTip(item)"
              width="15px"
              height="18px"
              alt=""
            />
            <span>{{ item.originalName }}</span>
          </div>
          <div class="upload_options">
            <span class="preview-class" @click="preview(item)">
              <i class="el-icon-view"></i>
              预览</span
            >
          </div>
        </div>
      </transition-group>
    </draggable>
    <script>
    //上传逻辑采用mixins混入,方便其他附件组件复用
    import { webUploaderMixin } from "@/components/upload/webUploader.js";
    export default {
        mixins: [webUploaderMixin],
    }
    </script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

webUploaderMixin.js

// 相关依赖需要下载
import md5 from "@/utils/md5.js"; //计算文件的md5
import axios from 'axios'
import Queue from 'promise-queue-plus';
import { ref } from 'vue'
// 文件上传分块任务的队列(用于移除文件时,停止该文件的上传队列) key:fileUid value: queue object
import { getTaskInfoUpload, initTaskUpload, preSignUrlUpload, preMergeUpload } from '@/api/webUploader.js';
export const webUploaderMixin = {
  data() {
    return {
      fileUploadChunkQueue: {}
    }
  },
  methods: {
    /**
     * el-upload 自定义上传方法入口
     */
    async customHttpRequest(options) {
      const file = options.file;
      const task = await this.getTaskInfo(file); //首先匹配文件的md5,查询该md5是否存在,存在则可以直接添加
      if (task) {
        const { finished, path, taskRecord, attach } = task
        const { fileIdentifier: identifier } = taskRecord
        //如果之前已经上传过则直接添加到附件列表
        if (finished) {
          let attachResponse = {
            code: 200,
            data: attach
          }
          //之前已经上传过该附件直接秒传赋值就可以
          this.handleSuccess(attachResponse)
          return path
        } else {
          const errorList = await this.handleUpload(file, taskRecord, options)
          if (errorList.length > 0) {
            this.msgError("文件上传错误");
            return;
          }
          // const { code, data, msg } =
          let upLoadRes = await preMergeUpload(identifier)
          if (upLoadRes.code === 200) {
            //上传完成
            this.handleSuccess(upLoadRes)
            return path;
          } else {
            this.msgError("文件上传错误");
          }
        }
      } else {
        this.msgError("文件上传错误");
      }
    },
    /**
     * 上传逻辑处理,如果文件已经上传完成(完成分块合并操作),则不会进入到此方法中
     */
    handleUpload(file, taskRecord, options) {
      let lastUploadedSize = 0; // 上次断点续传时上传的总大小
      let uploadedSize = 0 // 已上传的大小
      const totalSize = file.size || 0 // 文件总大小
      let startMs = new Date().getTime(); // 开始上传的时间
      const { exitPartList, chunkSize, chunkNum, fileIdentifier } = taskRecord

      // 获取从开始上传到现在的平均速度(byte/s)
      const getSpeed = () => {
        // 已上传的总大小 - 上次上传的总大小(断点续传)= 本次上传的总大小(byte)
        const intervalSize = uploadedSize - lastUploadedSize
        const nowMs = new Date().getTime()
        // 时间间隔(s)
        const intervalTime = (nowMs - startMs) / 1000
        return intervalSize / intervalTime
      }
      const uploadNext = async (partNumber) => {
        const start = new Number(chunkSize) * (partNumber - 1)
        const end = start + new Number(chunkSize)
        const blob = file.slice(start, end)
        const { code, detailMsg, msg } = await preSignUrlUpload({ identifier: fileIdentifier, partNumber: partNumber })
        if (code === 200 && detailMsg) {
          await axios.request({
            url: detailMsg,
            method: 'PUT',
            data: blob,
            headers: { 'Content-Type': 'application/octet-stream' }
          })
          return Promise.resolve({ partNumber: partNumber, uploadedSize: blob.size })
        }
        return Promise.reject(`分片${partNumber}, 获取上传地址失败`)
      }

      /**
       * 更新上传进度
       * @param increment 为已上传的进度增加的字节量
       */
      const updateProcess = (increment) => {
        increment = new Number(increment)
        const { onProgress } = options
        let factor = 1000; // 每次增加1000 byte
        let from = 0;
        // 通过循环一点一点的增加进度
        while (from <= increment) {
          from += factor
          uploadedSize += factor
          //百分比与 100 进行比较,取较小的值   更新进度
          const percent = Math.min((100, Number(Math.round(uploadedSize / totalSize * 100))))
          this.percentage = percent ? percent : 0

          onProgress({ percent: percent })
        }

        const speed = getSpeed();
        const remainingTime = speed != 0 ? Math.ceil((totalSize - uploadedSize) / speed) + 's' : '未知'
        console.log('剩余大小:', (totalSize - uploadedSize) / 1024 / 1024, 'mb');
        console.log('当前速度:', (speed / 1024 / 1024).toFixed(2), 'mbps');
        console.log('预计完成:', remainingTime);
      }

      return new Promise(resolve => {
        const failArr = [];
        const queue = Queue(5, {
          "retry": 3, //Number of retries
          "retryIsJump": false, //retry now?
          "workReject": function (reason, queue) {
            failArr.push(reason)
          },
          "queueEnd": function (queue) {
            resolve(failArr);
          }
        })
        // console.log("queue::: ", queue);
        this.fileUploadChunkQueue[file.uid] = queue
        this.uploadLoading = true
        for (let partNumber = 1; partNumber <= chunkNum; partNumber++) {

          const exitPart = (exitPartList || []).find(exitPart => exitPart.partNumber == partNumber)
          if (exitPart) {
            // 分片已上传完成,累计到上传完成的总额中,同时记录一下上次断点上传的大小,用于计算上传速度
            lastUploadedSize += new Number(exitPart.size)
            updateProcess(exitPart.size)
          } else {
            queue.push(() => uploadNext(partNumber).then(res => {
              // 单片文件上传完成再更新上传进度
              updateProcess(res.uploadedSize)
            }))
          }
        }
        if (queue.getLength() == 0) {
          // 所有分片都上传完,但未合并,直接return出去,进行合并操作
          resolve(failArr);
          return;
        }
        queue.start()
      })
    },
    /**
     * 获取一个上传任务,没有则初始化一个
     */
    async getTaskInfo(file) {
      let task;

      const identifier = await md5(file);
      const { code, data, msg } = await getTaskInfoUpload(identifier);
      if (code === 200) {
        task = data;
        if (!task) {
          const initTaskData = {
            identifier,
            fileName: file.name,
            totalSize: file.size,
            chunkSize: 10 * 1024 * 1024,
          };
          const { code, data, msg } = await initTaskUpload(initTaskData);
          if (code === 200) {
            task = data;
          } else {
            this.msgError("文件上传错误");
          }
        }
      } else {
        this.msgError("文件上传错误");
      }
      return task;
    },
       //上传成功
    handleSuccess(response, file, fileList) {
      if (response.code === 200) {
        setTimeout(() => {
          this.uploadLoading = false;
        }, 800);
        let item = response.data;
        this.$emit("update:approvalFileList", [...this.fileArr, item]);
        if (this.isAllowReturnSuccessEmit) {
          this.$emit("uploadBatchSuccess", item);
        }
      } else {
        fileList.pop();
      }
      this.loading = false;
    },
  },
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199

md5.js

import SparkMD5 from 'spark-md5'
import { Loading } from 'element-ui';
const DEFAULT_SIZE = 20 * 1024 * 1024
const md5 = (file, chunkSize = DEFAULT_SIZE) => {
  return new Promise((resolve, reject) => {
    const startMs = new Date().getTime();
    const loading = Loading.service({
      lock: true,
      text: '系统处理中,请稍后!',
      spinner: 'el-icon-loading',
      background: 'rgba(0, 0, 0, 0.7)'
    });
    let blobSlice =
      File.prototype.slice ||
      File.prototype.mozSlice ||
      File.prototype.webkitSlice;
    let chunks = Math.ceil(file.size / chunkSize);
    // console.log("file.size::: ", file.size);
    let currentChunk = 0;
    let spark = new SparkMD5.ArrayBuffer(); //追加数组缓冲区。
    let fileReader = new FileReader(); //读取文件
    fileReader.onload = function (e) {
      spark.append(e.target.result);
      currentChunk++;
      if (currentChunk < chunks) {
        loadNext();
      } else {
        const md5 = spark.end(); //完成md5的计算,返回十六进制结果。
        console.log('文件md5计算结束,总耗时:', (new Date().getTime() - startMs) / 1000, 's')
        loading.close()
        resolve(md5);

      }
    };
    fileReader.onerror = function (e) {
      loading.close()
      reject(e);
    };

    function loadNext() {
      console.log('当前part number:', currentChunk, '总块数:', chunks);
      let start = currentChunk * chunkSize;
      let end = start + chunkSize;
      (end > file.size) && (end = file.size);
      fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
    }
    loadNext();
  });
}

export default md5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

接口文件可以参考下面链接

原博主为vue3 elementplus minio springboot 代码实现,我根据自己需求改成了vue2+element版本供参考,里面也有后端代码,可以直接复制

参考地址:https://gitee.com/Gary2016/minio-upload

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/873410
推荐阅读
相关标签
  

闽ICP备14008679号