封装大文件分片上传组件

293 阅读1分钟

前言: 基于elementUI upload组件做的二次封装

1.文件上传前校验方法

该方法主要用于文件上传前的相关校验,其中包括:上传文件本身,以及对上传文件的相关配置。(通用方法,适用在不同场景文件上传时候,根据不同业务场景配置不同的上传前校验)

详细说明在代码片段中。

export function beforeUpload(file, obj) {
  /*
   * 参数说明:
   * file:必填 上传文件
   * obj:选填 文件格式要求的配置项目
   * */
  let isOk = false
  let isOverLength = false

  // 是否传递第二个参数
  if (obj === undefined) {
    obj = {}
  }
  // 赋默认值
  var option = {
    fileType: obj.fileType || ['txt', 'doc', 'docx', 'pdf', 'jpg', 'jpeg', 'gif', 'png', 'xlsx', 'xls', 'rar', 'zip', '7z'],
    unit: obj.unit || 'KB',
    min: obj.min || 0,
    max: obj.max || 20480,
    length: obj.length || 0,
    error: obj.error || '请上传小于20MB的txt/doc/docx/pdf/jpg/jpeg/gif/png/xlsx/xls/rar/zip/7z文件',
    lengthError: obj.lengthError || '您上传的文件名过长'
  }

  // 类型对比
  const fileType = file.name.substring(file.name.lastIndexOf('.') + 1)
  for (var i = 0; i < option.fileType.length; i++) {
    if (fileType === option.fileType[i]) {
      isOk = true
    }
  }

  // 大小限制
  let size = 0
  if (option.unit === 'KB') {
    size = file.size / 1024
  } else if (option.unit === 'MB') {
    size = file.size / 1024 / 1024
  }
  if (option.max > 0 && option.min > 0) {
    const isLt = size < option.max
    const isGt = size > option.min
    if (!(isLt && isGt)) {
      isOk = false
    }
  } else if (option.max > 0 && option.min === 0) {
    const isLt = size < option.max
    if (!isLt) {
      isOk = false
    }
  }

  // 文件名长度限制
  if (option.length > 0) {
    const fileNameLength = file.name.length
    if (parseInt(fileNameLength, 10) > parseInt(option.length, 10)) {
      isOverLength = true
    }
  }

  if (!isOk) {
    Message.error(option.error)
  } else if (isOverLength) {
    Message.error(option.lengthError)
  }
  return isOk && !isOverLength
}

2.对上传文件内容进行加密方法

采用的是spark-md5

安装npm包

npm i --save spark-md5

export function calculateMd5(file, callBack) {
  const fileReader = new FileReader()
  const blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice
  const chunkSize = 2097152
  // read in chunks of 2MB
  const chunks = Math.ceil(file.size / chunkSize)
  let currentChunk = 0
  const spark = new SparkMD5()

  fileReader.onload = function (e) {
    spark.appendBinary(e.target.result) // append binary string
    currentChunk++

    if (currentChunk < chunks) {
      loadNext()
    } else {
      callBack(spark.end())
    }
  }

  function loadNext() {
    const start = currentChunk * chunkSize
    const end = start + chunkSize >= file.size ? file.size : start + chunkSize
    fileReader.readAsBinaryString(blobSlice.call(file, start, end))
  }

  loadNext()
}

3.上传组件的主体

其中主要功能涉及到文件分片上传时候的几种不同情况分别如何处理,上传前校验(在第一个内容中),上传前对文件加密(在第二个内容中)

<template>
  <div class="chunk-upload">
    <el-upload
      ref="dialogUpload"
      class="upload"
      action="/"
      :before-upload="checkBeforeUpload"
      :show-file-list="false"
      :http-request="httpChunks"
      drag
    >
      <!-- <i class="el-icon-upload"></i> -->
      <svg-icon icon-class="upload" style="font-size:110px;" />
      <div class="upload__text">
        点击选择或拖拽文件到这里上传
        <br />
        <!-- 支持扩展名:.rar .zip .xls .xlsx .pdf .jpg .png .tif -->
        <span>支持.pdf/.tif/.zip/.rar/.7z格式</span>
      </div>
    </el-upload>
  </div>
</template>
<script>
import { calculateMd5, beforeUpload } from '@/utils/tool.js'
import { chunkUploadApi, fileMerge } from '@/api/common'
export default {
  name: 'ChunkUpload',
  props: {
    preventUpload: {
      type: Boolean
    },
    fileList: {
      // 批量上传的数据列表
      type: Array
    },
    paramsInsurance: {
      type: Object
    }
  },
  computed: {
    list() {
      return this.fileList
    }
  },
  data() {
    return {
      uploadSettings: {
        eachSize: 2 * 1024 * 1024 // 每块文件大小
      },
      file_md5: null
    }
  },
  methods: {
    // 自定义的文件上传
    async httpChunks(options) {
      const { file } = options // options有file对象、onProgress、onError等
      await this.fileSplitAndUpload(file)
    },
    // 大文件分块上传
    fileSplitAndUpload(file) {
      // 上传前先md5加密 然后校验这个文件是否上传过 如果上传过,有两种状态-全部上传成功、上传到某一片
      const that = this
      file.status = 'uploading'
      // 控制fileList只有1个文件
      if (that.list.length === 1) {
        that.list.splice(0, 1)
      }
      that.list.push(file)
      that.$emit('changeList', that.list)
      calculateMd5(file, function(val) {
        that.file_md5 = val
        // 总片数
        const chunkCount = Math.ceil(file.size / that.uploadSettings.eachSize)
        // 文件切片
        const fileChunks = that.splitFile(file, that.uploadSettings.eachSize, chunkCount)
        // chunkNum从1开始
        that.chunkUpload(1, fileChunks, chunkCount, file, that.uploadSettings.eachSize, that.file_md5, 0)
      })
    },
    // 分片上传
    chunkUpload(chunkNum, fileChunks, chunkCount, file, eachSize, md5, progress) {
      const currentChunk = fileChunks[chunkNum - 1]
      // 串行上传 如果从未上传过此文件 则从第一片开始上传 如果上传过且没上传完毕 是由后端返回从几片开始
      const params = {
        chunkNum, // 第几片
        chunkCount, // 总片数
        fileMd5: md5, // 整个文件的md5
        fileName: file.name, // 文件名
        fileSize: file.size, // 文件总大小
        multipartFile: currentChunk,
        coverageCode: this.paramsInsurance.coverageCode,
        insuranceLtd: this.paramsInsurance.insuranceLtd
      }
      // 请求分片上传的接口
      const formData = new FormData()
      for (const p in params) {
        if (params.hasOwnProperty(p)) {
          formData.append(p, params[p])
        }
      }
      chunkUploadApi(formData)
        .then(res => {
          if (res.status === 200) {
            switch (res.data.code) {
              case 1: // 上传全部完成且分片已合并
                this.list.forEach(item => {
                  if (item.uid === file.uid) {
                    item.percentage = 100
                    item.status = 'success'
                  }
                })
                this.$emit('changeList', this.list)
                this.$emit('uploadSuccess')
                this.$message.success('文件上传成功')
                break
              case 3: // 上传完成,分片待合并
                // 调用合并接口
                fileMerge({ fileMd5: md5 })
                  .then(res => {
                    if (res.status === 200) {
                      this.list.forEach(item => {
                        if (item.uid === file.uid) {
                          item.percentage = 100
                          item.status = 'success'
                        }
                      })
                      this.$emit('changeList', this.list)
                      this.$emit('uploadSuccess')
                      this.$message.success('文件上传成功')
                    } else {
                      this.list.forEach(item => {
                        if (item.uid === file.uid) {
                          item.status = 'fail'
                        }
                      })
                      this.$emit('changeList', this.list)
                      this.$message.error(res.message || '文件上传失败')
                    }
                  })
                  .catch(() => {
                    this.$message.error('文件分片合并异常')
                  })
                break
              case 0: // 上传中,继续上传下一个分片
                let percent = 0
                this.list.forEach(item => {
                  if (item.uid === file.uid) {
                    item.percentage = 100 - (res.data.restChunkCount / chunkCount) * 100
                    percent = item.percentage
                  }
                })
                this.$emit('changeList', this.list)
                this.chunkUpload(res.data.nextFileChunk, fileChunks, chunkCount, file, eachSize, md5, percent)
                break
              default:
                this.$message.error('文件上传异常')
                break
            }
          } else {
            this.list.forEach(item => {
              if (item.uid === file.uid) {
                item.status = 'fail'
              }
              // item.percentage = 0
            })
            this.$emit('changeList', this.list)
            this.$message.error(res.message || '文件上传失败')
            return false // 如果当前片上传出错 就不再往下继续
          }
        })
        .catch(err => {
          this.list.forEach(item => {
            if (item.uid === file.uid) {
              item.status = 'fail'
            }
          })
          this.$emit('changeList', this.list)
          if (err && err.message === '停止文件上传') {
            // this.$message.success('停止文件上传成功') // 暂不提示
          } else {
            console.log(err)
            this.$message.error('文件上传异常,请稍后再试')
          }
          return false // 如果当前片上传出错 就不再往下继续
        })
    },
    // 文件分块,用Array.prototype.slice方法
    splitFile(file, eachSize, chunkCount) {
      const fileChunk = []
      for (let chunk = 0; chunkCount > 0; chunkCount--) {
        fileChunk.push(file.slice(chunk, chunk + eachSize))
        chunk += eachSize
      }
      return fileChunk
    },
    checkBeforeUpload(file) {
      if (this.preventUpload === true) return false
      return beforeUpload(file, {
        fileType: ['pdf', 'tif', 'zip', 'rar', '7z', 'PDF', 'TIF', 'ZIP', 'RAR', '7Z'], // 上传限制的文件类型
        unit: 'MB', // 单位 支持KB,MB
        min: 0, // 最小值 可不传递
        max: 200, // 最大值 可不传递
        error: '请上传小于200MB的.pdf/.tif/.zip/.rar/.7z文件' // 错误提示文案
      })
    }
  }
}
</script>