大文件分片上传

294 阅读1分钟

如果文件很大, 会需要分片上传, 原理是用File.prototype.slice或者Blob.prototype.slice去切分二进制的文件数据, 然后用FileReader读取这些数据片段, 然后获取这些片段的md5, 然后开始传输, 如果需要merge, 后端会返回对应的相应, 前端再请求merge的接口, 将分开的多个文件片段merge成一个文件.

使用插件: vue-simple-uploader

下面是封装好的组件:

<template>
  <div>
    <div id="status" style="position:relative;"></div>
    <uploader ref="uploader"
        :options="totalOptions"
        :autoStart="true"
        @file-added="onFileAdded"
        @file-success="onFileSuccess"
        @file-progress="onFileProgress"
        @file-error="onFileError">
        <uploader-drop>
            <p style="margin:0">选择上传文件类型</p>
            <uploader-btn style="margin-right:10px" :attrs="totalOptions.attrs">选择上传文件</uploader-btn>
            <uploader-btn :directory="true" v-if="totalOptions.selectDirectory">选择上传文件夹</uploader-btn>
        </uploader-drop>
        <uploader-list></uploader-list>
    </uploader>
  </div>
</template>
<script>
import SparkMD5 from 'spark-md5'
import { uploadFileMerge } from '@/api/uploader'
import $ from 'jquery'
import { getToken } from '@/utils/auth'

export default {
  props: {
    options: {
      type: Object,
      default () {
        return {}
      }
    }
  },
  data () {
    return {
      defaultOptions: {
        target: window.location.origin + window.location.pathname + 'xxx',
        chunkSize: 1 * 1024 * 1000,
        fileParameterName: 'file',
        maxChunkRetries: 2,
        testChunks: true, // 是否开启服务器分片校验
        checkChunkUploadedByResponse: function (chunk, message) {
          // 服务器分片校验函数,秒传及断点续传基础
          const objMessage = JSON.parse(message)
          if (objMessage.skipUpload) {
            return true
          }
          return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
        },
        multiple: false,
        headers: {
          'Auth-Token': getToken()
        },
        query () {
        },
        attrs: {
          accept: [] //可上传的文件类型
        }
      }

    }
  },
  methods: {
    onFileAdded (file) {
      //清空文件列表, 目的是让用户感知到只可以上传一个文件
      if (!this.totalOptions.multiple) {
        this.uploader.fileList = []
        this.uploader.fileList.push(file)
      }
      console.log(file, 'file=====')
      this.computeMD5(file)
    },
    onFileProgress (rootFile, file, chunk) {
      console.log('... onFileProgress')
    },
    onFileSuccess (rootFile, file, response, chunk) {
      const res = JSON.parse(response)
      console.log('... onFileSuccess %o', res)
      console.log(this.uploader, 'uploader')
      // 如果服务端返回需要合并
      if (res.needMerge) {
        console.log('... onFileSuccess needMerge')
        // 文件状态设为“合并中”
        this.statusSet(file.id, 'merging')
        const param = {
          'filename': rootFile.name,
          'identifier': rootFile.uniqueIdentifier,
          'totalSize': rootFile.size
        }
        uploadFileMerge(param).then(res => {
          this.statusRemove(file.id)
          console.log('... onFileSuccess merge done')
          this.$emit('uploadSuccess', res.data)
        }).catch(e => {
          console.log('合并异常,重新发起请求,文件名为:', file.name)
          file.retry()
        })
      } else {
        this.$emit('uploadSuccess', res.data)
      }
    },
    onFileError (rootFile, file, response, chunk) {
      console.log('... onFileError')
    },
    computeMD5 (file) {
      const fileReader = new FileReader()
      const time = new Date().getTime()
      const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
      let currentChunk = 0
      const chunkSize = 10 * 1024 * 1000
      const chunks = Math.ceil(file.size / chunkSize)
      const spark = new SparkMD5.ArrayBuffer()

      // 文件状态设为"计算MD5"
      this.statusSet(file.id, 'md5')
      file.pause()

      loadNext()

      fileReader.onload = e => {
        spark.append(e.target.result)

        if (currentChunk < chunks) {
          currentChunk++
          loadNext()

          // 实时展示MD5的计算进度
          this.$nextTick(() => {
            $(`.myStatus_${file.id}`).text('校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%')
          })
        } else {
          const md5 = spark.end()
          this.computeMD5Success(md5, file)
          console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`)
        }
      }

      fileReader.onerror = function () {
        this.error(`文件${file.name}读取出错,请检查该文件`)
        file.cancel()
      }

      function loadNext () {
        const start = currentChunk * chunkSize
        const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
        console.log('[loadNext] currentChunk=%d start=%d end=%d', currentChunk, start, end)
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
      }
    },
    statusSet (id, status) {
      const statusMap = {
        md5: {
          text: '校验MD5',
          bgc: '#fff'
        },
        merging: {
          text: '合并中',
          bgc: '#e2eeff'
        },
        transcoding: {
          text: '转码中',
          bgc: '#e2eeff'
        },
        failed: {
          text: '上传失败',
          bgc: '#e2eeff'
        }
      }

      console.log('.....', status, '...:', statusMap[status].text)
      this.$nextTick(() => {
        $(`<p class="myStatus_${id}"></p>`).appendTo(`#status`).css({
          'position': 'absolute',
          'top': '0',
          'left': '0',
          'right': '0',
          'bottom': '0',
          'zIndex': '1',
          'line-height': 'initial',
          'backgroundColor': statusMap[status].bgc
        }).text(statusMap[status].text)
      })
    },
    computeMD5Success (md5, file) {
      Object.assign(this.uploader.opts, {
        query: {
          ...this.params
        }
      })
      file.uniqueIdentifier = md5
      file.resume()
      this.statusRemove(file.id)
    },
    statusRemove (id) {
      this.$nextTick(() => {
        $(`.myStatus_${id}`).remove()
      })
    }
  },
  computed: {
    // Uploader实例
    uploader () {
      return this.$refs.uploader.uploader
    },
    totalOptions () {
      return { ...this.defaultOptions, ...this.options }
    }
  }
}
</script>