对前端切片上传操作进行封装

72 阅读2分钟

需求

  1. 将文件切片上传 / 如何切片 / 如何确定切片的数量 / 如何确定切片的大小
    • 使用 Blob.protype.slice(),因为file的原型是Blob
    • 以切片数量优先,先规定 maxCountmaxSize;如果 file.size / maxSize 大于 maxCount,则使用 file.size / maxCount 来重新计算 maxCount
  2. 何时通知后端将切片合并
    • 在所有切片都上传之后,使用一个接口通知后端合并 (需要按照顺序传输)
    • 在上传切片的接口中传送当前切片是第几个,总共有多少的切片 (需要按照顺序传输)
    • 在前端切片都上传之后,使用一个接口获取后端获取状态,后端返回已经获取的切片,前端过滤出还没有上传的切片,重新上传一次 (可以并发传输),需要限制重新上传的次数,否则会导致死循环
  3. 显示上传进度条
    • 此次上传的切片次序除以切片总数 (index/count)
  4. 上传中止之后继续上传 (断点续传)

步骤

封装成一个实现类

constructor() {
  this.id = ''
  this.md5 = ''
  this.chunkList = []
  // 已经成功上传的切片列表,用于验证上传时
  this.uploadedChunkList = []
  // Number of current failures
  this.count = 0
  this.progress = 0
}

const fileChunk = new FileChunk()
export default fileChunk
  1. 获取文件

    • 使用上传组件
    • 自己定义上传文件的组件
  2. 将文件进行切片,存入 chunkList

    createFileChunk(file, maxSize = 1024 * 1024) {
        // 判断文件大小,超过多少才进行分片上传
        if (file.size > maxSize) {
          // 限制上传的切片数量,限制上传的大小
          let max = maxSize,
            count = Math.ceil(file.size / maxSize),
            index = 0,
            chunkList = []
          if (count > 18) {
            max = Math.ceil(file.size / 18)
            count = 18
          }
          while (index < count) {
            (index + 1) * max < file.size
              ?
              chunkList.push(
                {
                  name: file.name,
                  start: index * max,
                  chunks: count,
                  chunk: index,
                  file: file.slice(index * max, (index + 1) * max)
                }
              )
              :
              chunkList.push(
                {
                  name: file.name,
                  start: index * max,
                  chunks: count,
                  chunk: index,
                  file: file.slice(index * max, file.size)
                }
              )
          }
          return chunkList
        } else {
            // 一次性上传操作
        }
    }
    
  3. 并发发送

    // 上传切片 
    uploadChunkList(chunks) {
        // 获取已经上传的切片的index
        const uploadedChunkIndex = this.uploadedChunkList.map((item) => item.chunkIndex)
        // 过滤出没有上传成功的切片
        const chunkList = this.chunkList.filter((item) => !item.find(uploadedChunkIndex))
        // 并发上传,限制并发数量6(因为浏览器每个域最多允许6个链接<分浏览器,有些是,有些不是,但是一般都是>)
        new Promise((resolve) => {
          const length = chunkList.length
          let index = 0
          let counter = 0
          let max = 6
          const start = async () => {
            while (index < length && max > 0) {
              // 同步代码
              max--
              const formData = new FormData()
              formData.append('name', chunkList[index].name)
              formData.append('start', chunkList[index].start)
              formData.append('chunks', chunkList[index].chunks)
              formData.append('chunk', chunkList[index].chunk)
              formData.append('file', chunkList[index].file)
              index++
              // 异步代码
              await uploadChunkPromise(formData).then(res => {
                this.id = res.data
                counter++
                max++
                // Keep the decimal place to one place
                this.progress = Math.floor((index + 1) / length * 1000) / 10
                if (counter === length) {
                  resolve()
                } else {
                  start()
                }
              })
            }
          }
          start()
        })
    }
    
  4. 获取上传结果,上传失败,上传成功,上传部分成功,分别进行处理

    async getUploadResult() {
        // 只能失败三次
        const id = this.id
        const res = await getUploadStatusPromise(id)
        if (res.status === 0 && this.count < 3) { // 不成功,有切片未上传成功
          this.count++
          // 执行上传
          this.uploadedChunkList = res.uploadedChunkList
          this.uploadChunkList()
          // 判断状态
          await this.getUploadResult(id)
        } else if (res.status === 1) { // 上传成功
          this.progress = 'success'
        } else {
          this.progress = 'fail'
        }
    }
    
  5. 清除数据

    clearData() {
        this.id = ''
        this.md5 = ''
        this.chunkList = []
        this.uploadedChunkList = []
        this.count = 0
        this.progress = 0
    }
    
  6. 创建md5

    使用 object-hash 库生成md5,一共有40位

    calculateFileMd5(file) {
        return hash(file) + (new Date()).valueOf()
    }
    
  7. 入口方法

    async doUpload(file, size) {
        this.clearData()
        this.chunkList = this.createFileChunk(file, size)
        await this.uploadChunkList()
        await this.getUploadResult()
        return this.progress
    }