最简单的前端大文件上传

673 阅读1分钟

网上已经有了很多关于大文件上传的技术解决文案,但是有一个难点没有解决,大部分都是直接将大文件直接分成n份然后全部上传(如果n特别大是怎么办?),但是实际上用户可能把大文件切为n份,每次上传m(m < n)个文件。这里主要记录一下此问题的关键技术点。

项目需求

首先是实现原理,1个G的大文件如果在上传至服务器的时候分成10份来传输,在带宽够用的情况下,理论上的上传速度可以提升10倍。

初始需要定义的属性

return {
    // 被上传的文件
    file: {},
    // 表示切分文件的集合
    chunks: [],
    // 文件上传进度
    uploadProgress: 0,
    // chunks上传序号
    uploadNum: 0,
    // 已经上传完成的切片数
    uploadedNum: 0,
    // 文件上传并发数
    maxUploadNums: 10,
    // 文件切片大小
    splieSize: 1024 * 1024 * 5,
    // 文件MD5校验值
    md5: ''
}

切分文件的方法

function sliceFile (file, piece = 1024 * 1024 * 5) {
  // piece表示文件被切割的单个大小,默认5M,支持自定义大小
  // 文件总大小
  const totalSize = file.size
  // 每次上传的开始字节
  let start = 0
  // 每次上传的结尾字节
  let end = start + piece
  // chunks表示切分文件的集合
  const chunks = []
  while (start < totalSize) {
    const blob = file.slice(start, end)
    chunks.push(blob)
    start = end
    end = start + piece
  }
}

生成文件MD5(文件MD5提交给后端,后端在合并分片后用以校验文件是否和前端上传的文件一致)

// 需要使用spark-md5
import SparkMD5 from 'spark-md5'
function generateMD5 (file) {
    const spark = new SparkMD5.ArrayBuffer()
    const fileReader = new FileReader()
    let currentChunk = 0
    const chunkSize = this.splieSize
    const chunksNum = Math.ceil(file.size / chunkSize)
    const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
    fileReader.onload = (e) => {
        console.log('read chunk nr', currentChunk + 1, 'of', chunksNum)
        spark.append(e.target.result)
        currentChunk++
        if (currentChunk < chunksNum) {
          loadNext()
        } else {
          this.fileMD5 = spark.end()
          console.log(this.fileMD5)
        }
    }
    fileReader.onerror = function () {
        console.warn('oops, something went wrong.')
    }
    function loadNext () {
        const start = currentChunk * chunkSize
        const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
        fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
    }
    loadNext()
}

开始上传

startUpload () {
  const lth = this.chunks.length > this.maxUploadNums ? this.maxUploadNums : this.chunks.length
  this.uploadNum = 0
  this.uploadedNum = 0
  for (let i = 0; i < lth; i++) {
    this.uploadChunk({
      serial: this.file.uid,
      chunk: this.uploadNum,
      file: this.chunks[this.uploadNum]
    })
    this.uploadNum++
  }
},

上传文件的方法

uploadChunk (params = {}) {
    const form = new FormData()
    form.append('serial', params.serial)
    form.append('chunk', params.chunk)
    form.append('file', params.file)
    const { code } = await uploadFileAPI(form)
    if (code === 200) {
      this.uploadedNum++
      if (this.uploadNum < this.chunks.length) {
        this.uploadChunk({
          serial: this.file.uid,
          chunk: this.uploadNum,
          file: this.chunks[this.uploadNum]
        })
        this.uploadNum++
      } else if (this.uploadedNum === this.chunks.length) {
        // 切片上传完成,开始合并切片
        this.mergeFile({
          id: this.file.uid
        })
      }
    }
},

合并文件

async mergeFile (params = {}) {
  params.md5 = this.fileMD5
  const { code } = await mergeFileAPI(params)
  if (code === 200) {
    // 合并成功
  }
}