0.需求背景
- 遇到大文件上传时,会存在文件过大,后端无法一次性接受
 - 上传过程中,异常失败后,需要重新上传,耗时
 - 单次请求时间过长,请求受限
分片上传,相比于普通的单线程上传,速度更快,更灵活。 
1.大文件上传的解决思路
- 前端文件切片:把一个大文件转换成二进制内容,然后按照一个固定的大小对二进制内容进行切割,得到多个小文件,然后循环上传所有的小文件。在js中,文件File对象是Blob对象的子类,可以使用slice()方法完成对文件的切割;
 - 后端文件合并:当所有小文件上传完成,调用接口通知后端把所有的文件按编号进行合并,组成大文件;
 - 并发控制:结合Promise.race和异步函数实现,限制多个请求同时并发的数量,防止浏览器内存溢出;
 - 断点续传:把所有上传失败的小文件加入一个数组里面,在所有小文件都上传结束(成功和失败都算结束)之后再上传一次上传失败了的小文件,反复执行这一步,直到所有小文件都上传成功,可以通过递归实现。
 
思考的点
- 上传多次失败的分片,如何处理? 
  
- 如上思路,会自动重新上传,但是不可以无限制的一直重复上传。大概率是接口并发问题,二次上传或者调整并发量,就可以上传上去了。
 - 提供重新上传机会,但是原则上,必须所有的分片都上传成功后,才能合并出最终的大文件。
 
 - 断网重连如何处理?待实现 
  
- 出现请求失败的请求,则循环停止,暂停后面的请求了,直到网络连接后再继续上传!定时器,判断网络是否可行。
 - 暴力的方案:界面检测到没有网络了,直接提示让客户重新上传,原来上传的作废。
 
 - 上传一半,浏览器关闭,已上传的切片垃圾数据,如何处理? 
  
- 方案1,界面销毁,触发销毁事件,告知后台,删除掉剩下的文件
 - 方案2,后台定时检查分片数据。如果文件夹的创建日期是隔天的,则删除。
 - 客户要二次上传,就重新进界面了
 
 - 进度显示,切片的上传返回结果
 
3.核心代码参考
前端大文件切片
// 文件分片  
let hash = 0 // 切片序号  
let size = 1024 * 50 // 切片大小  
let fileArr = []  
  
total.value = Math.ceil(file.size / size)  
console.log('total: ', total)  
  
for (let i = 0; i < file.size; i = i + size) {  
  fileArr.push({  
    hash: hash++,  
    chunk: file.slice(i, i + size),  
    uploadTimes: 0  
  })  
  progress.value = Math.round(hash / total.value * 10000) / 100  
  console.log('拆分', hash)  
  console.log('拆分进度', progress.value + '%')  
}
 
前端切片上传
// 遍历文件列表,上传文件  
for (let i = 0; i < list.length; i++) {  
  let item = list[i]  
  if (item.uploadTimes > 5) {  
    errorList.push(item)  
    console.log('item.uploadTimes: ', item.uploadTimes)  
    continue  
  }  
  item.uploadTimes = item.uploadTimes + 1//记录  
  
  let formData = new FormData()  
  formData.append('filename', file.name)  
  formData.append('hash', item.hash)  
  formData.append('dir', uuid)  
  formData.append('chunk', item.chunk)  
  console.log('上传', item.hash)  
  
  // 上传  
  let res = axios({  
    method: 'post',  
    url: 'bigfile/upload',  
    data: formData  
  })  
  // 把上传文件的异步操作放入并发池里  
  pool.push(res)  
  if (pool.length === max) {  
    // 每当并发池跑完一个任务,就再塞入一个任务  
    await Promise.race(pool)  
  }  
  
  res.then((response) => {  
    let status = response.data.status  
    if (status == '201') {  
      // 请求成功,从并发池里移除  
      const index = pool.findIndex(it => it === res)  
      pool.splice(index, 1)  
      finalFine.value++  
      progress.value = Math.round(finalFine.value / total.value * 10000) / 100  
      console.log('response: ', response)  
      console.log('上传进度 ----- ', finalFine.value, total.value, progress.value + '%')  
    } else {  
      //上传有问题  
      const index = pool.findIndex(it => it === res)  
      pool.splice(index, 1)  
      failList.push(item)  
    }  
  }).catch((response) => {  
    console.log('response-error: ', response)  
    // 请求失败,从并发池里移除,添加到失败的文件列表  
    const index = pool.findIndex(it => it === res)  
    pool.splice(index, 1)  
    failList.push(item)  
  }).finally(() => {  
    finish++  
    // 如果请求都完成了,递归调用自己,把上传失败的文件列表再上传一次  
    if (finish === list.length) {  
      uploadFileChunks(failList, uuid)  
    }  
  })  
}
 
后端切片合并
// 创建目标文件  
RandomAccessFile writeFile = new RandomAccessFile(targetFile, "rw");  
  
// 位置起始点  
long position = 0L;  
for (String tempFilename : fileNames) {  
    // System.out.println(tempFilename);  
    File sourceFile = new File(parentDir, tempFilename);// 切片文件  
    RandomAccessFile readFile = new RandomAccessFile(sourceFile, "rw");  
    int chunksize = 1024 * 3;  
    byte[] buf = new byte[chunksize];  
    writeFile.seek(position);  
    int byteCount = 0;  
    while ((byteCount = readFile.read(buf)) != -1) {  
       if (byteCount != chunksize) {  
          byte[] tempBytes = new byte[byteCount];  
          System.arraycopy(buf, 0, tempBytes, 0, byteCount);  
          buf = tempBytes;  
       }  
       writeFile.write(buf);// 写入  
       position = position + byteCount;  
    }  
    readFile.close();  
}  
writeFile.close();
 
4.案例源码
百度网盘 链接:https://pan.baidu.com/s/1wMJoE0ETiSPHniJLV5IPZA
码微信小程序。获取 提取码




















