前端大文件上传-分片-并发-断点-秒传

184 阅读2分钟

问题分析

文件过大一次性上传如果中断,只能重来,体验极差。

如果已经上传过,再次上传,浪费存储空间,浪费用户时间。

如此,我们需要做一个支持断点续传,同时支持秒传的方法。

实现思路

后端使用md5标记每一个上传文件,每次上传文件时,前端计算文件md5,然后计算文件分片数,请求后端,如果存在,则秒传。

如果不存则返回当前md5文件上传所有分片的上传地址列表(url带一次性上传权限,防止有人利用恶意利用上传文件接口占用空间),以及如果文件上传过,没上传完,则返回应该从第几个分片继续上传。

然后开启分片上传的并发控制,上传完毕,进行文件合并。

代码参考

选择文件

<input   @change='addFile' type="file" >


let chunkSize=6*1024*1024//大于6MB的分片上传
addFile(event) {
    let file = event.target.files[0]  
        let size = file.size; 
        this.loading = true 
        if(file.size>chunkSize){ 
            //如果大于基本分片单位大小,分片上传
          this.uploadFile(file,event,(str)=>{
            this.uploadMessage=str 
          }) 
        }else{
         //小于基本分片单位大小,直接上传即可,也无需合并 
         let param = new FormData();
         param.append('file', file);
        utilsApi.postMinio(param).then(res => {             //上传成功  
            this.loading = false 
        }).catch(e => { 
            this.loading=false
        })
      } 
}            

MD5计算

这里使用的依赖:"spark-md5": "^3.0.1"

  /**
 * 获取文件MD5
 * @param file
 * @returns {Promise<unknown>}
 */
 import SparkMD5 from 'spark-md5'
 getFileMd5(file){
      let fileReader = new FileReader()
      fileReader.readAsBinaryString(file)
      let spark = new SparkMD5()
      return new Promise((resolve) => {
          fileReader.onload = (e) => {
              spark.appendBinary(e.target.result)
              resolve(spark.end())
          }
      })
  }

分片和判断上传类型

uploadFile(file,callback){
  //文件大小
  const fileSize = file.size 
  //计算当前选择文件需要的分片数量
  let chunkCount = Math.ceil(fileSize / chunkSize)-1
  chunkCount=chunkCount>0?chunkCount:1
  // console.log("文件大小:",(file.size / 1024 / 1024) + "Mb","分片数:",chunkCount) 
  let that=this
  //获取文件md5
  this.getFileMd5(file).then(fileMd5=>{
    // console.log("文件md5:",fileMd5)
    // console.log("向后端请求本次分片上传初始化")
    //向后端请求本次分片上传初始化
    const initUploadParams = JSON.stringify({chunkCount: chunkCount,fileMd5: fileMd5})
    utilsApi.sliceUpload(initUploadParams).then(res=>{ 
        this.count=0
         if (res.data == null) {
            // console.log("当前文件上传情况:所有分片已在之前上传完成,仅需合并")
            this.composeFile(fileMd5,file.name,event)
            return;
        }
        if (res.msg == "上传完成") { 
             console.log("file url:"+res.data)             // console.log("当前文件上传情况:秒传")
            this.loading=false 
            return
        }
        const chunkUploadUrls = res.data 
        //正常开始并发上传
        this.multiRequest(chunkUploadUrls,5,file,fileMd5).then(val=>{
          // 上传完毕合并分片
          this.composeFile(fileMd5,file.name,event)
        }).catch(err=>{           that.loading=false
        })

    }).catch(val=>{ 
      that.loading=false
    })
  }).catch(err=>{ 
      that.loading=false
  })
},

并发请求控制

//并发控制
multiRequest(chunkUploadUrls = [], maxNum,file,fileMd5) {
  // 请求总数量
  const len = chunkUploadUrls.length;
  // 根据请求数量创建一个数组来保存请求的结果
  const result = new Array(len).fill(false);
  // 当前完成的数量
  let count = 0;
  // console.log("开始promise")
  let that=this 
  let finish=0//完成数量
  return new Promise((resolve, reject) => {
    // 请求maxNum个
    while (count < maxNum) {
      next();
    }
//递归和promise控制并发请求数量,启动maxNum个请求,每完成一个启动一个新的
//直到所有的请求完毕,然后请求合并接口
   function next(){
     try{ 
      let ind = count++;
      // 处理边界条件
      if (ind >= len ) {
        // console.log("完成了")
        // 请求全部完成就将promise置为成功状态, 然后将result作为promise值返回
        !result.includes(false) && resolve(result);
        return;
      }
        // console.log(`开始 ${ind}`, new Date().toLocaleString());
        let item=chunkUploadUrls[ind]
        //分片开始位置
        let start = (item.partNumber - 1) * chunkSize
        //分片结束位置
        let end = 0
        if(ind==chunkUploadUrls.length-1){
          end=file.size
        }else{
          end =start + chunkSize
        }
        //取文件指定范围内的byte,从而得到分片数据
        let _chunkFile = file.slice(start, end)
        // console.log("end-start:",end-start) 
        let last=0
        let param = new FormData(); //创建form对象 
        let url=item.url 
        param.append('key',fileMd5+"-"+(ind+1)+".chunk");
        param.append('file', _chunkFile); 
        utilsApi.postMinio({file:param,url:url }).then(res=>{
          finish++
          // console.log("第" +  that.count+ "个分片上传完成")
          // that.uploadMessage="上传"+ (that.count/  chunkUploadUrls.length * 100 | 0) + '%'
          result[ind] = true;
          // console.log(`完成 ${ind}`, new Date().toLocaleString()); 
          // 请求没有全部完成, 就递归
          if (ind < len) {
            next();
          }
        }).catch(err=>{
           reject("上传中断")
        })  
        }catch(err){
            //异常中断
          reject(err)
        } 
    }
  });