记一次文件上传的曲折历史

工具

  • Vue
  • element-UI
  • spark-md5
  • axios

需求

上传pdf文件,为了节省时间,对文件进行切片处理,对上传进度进行监控(这里挺有趣)

实现过程

1 文件切片

思路:使用了File.slice()对文件进行切片处理,将每一份切片放到一个数组中,然后使用Array.map()方法配合axios将每一段切片上传至服务器

HTML主体(pug)

el-upload(
  ref="resource",
  drag
  :file-list="resourcefileList",
  :before-upload="resourceBeforeUpload",
  accept=".pdf",
  :limit="1",
  action="",
  :http-request="customUpload",
  :on-remove="removeList"
)
  i.el-icon-upload
    .el-upload__text 将文件拖到此处,或
      em 点击上传

使用element-ui的upload组件,使用自定义事件 http-request,在customUpload中对文件进行操作

customUpload(content) {
  // 覆盖默认上传事件
  let file = content.file
  if (!file) return
  // 将切片固定成10分,也可以固定大小上传
  let axiosArray = []
  let chunkList = []
  let chunkSize = file.size / 10
  let current = 0
  let fileName = this.formData.identity + '.pdf'
  while (current < 10) {
    chunkList.push({
      chunk: file.slice(current * chunkSize, (current + 1) * chunkSize),
      fileName: current + "-" + fileName
    })
    current++
  }
  // 初始化数据
  this.percentage = [0,0,0,0,0,0,0,0,0,0]
  this.uploadOver = false
  this.progressVisable = true
  // 切片并发传给后端,要注意切片上传时请求头是 multipart/form-data 合并切片时请求头是x-www-form-urlencoded,只能上传键值对
  chunkList.map((item) => {
    const index = parseInt(item.fileName.split('-')[0])
    let form = new FormData()
    form.append("file", item.chunk, item.fileName)
    form.append("fileName", this.formData.identity)
    axiosArray.push(
      this.$http.post("/upload/part", form, {
        headers: { "Content-Type": "multipart/form-data" },
      }).then(res=> {
        res
        // this.percentage += 10
      })
    )
  })
  // 所有切片上传成功后合并
  Promise.all(axiosArray).then(res => {
    res
    this.$http.get(`/upload/merge?fileName=${this.formData.identity}`).then(res => {
      if(res) {
        // console.log(res);
        // this.progressVisable = false
        this.formData.resource = res.data.obj
        this.uploadOver = true
        this.overContent = '上传成功,感谢支持'
      }
    }).catch(err => {
      err
      this.uploadOver = true
      this.overContent = '传输失败,请重试'
    })
  })
},

这段代码应该很清楚明白了,代码中的identity是文件的MD5值,利用spark-md5计算,目的是为了防止重复文件的上传,计算方法如下

computeMd5(file) {
  // 计算文件MD5
  const fread = new FileReader()
  const spark = new SparkMD5.ArrayBuffer()
  return new Promise((resolve, reject) => {
    if(!file) {
      reject('no file')
    }
    fread.readAsArrayBuffer(file)
    fread.onload = (e) => {
      spark.append(e.target.result)
      const md5 = spark.end()
      resolve(md5)
    }
  })
},

这里有一个小插曲,fread.onload是一个异步函数,所以在计算md5值的时候会出现null的情况,这里返回一个Promise,配合调用时的async/await实现同步操作

2 进度条实现

原始思路:一共分了10片,每一片上传成功后将进度+10,所有文件上传完成之后就是100%了(你可能一眼看出了这里面的问题,所以我说的原是思路)

问题分析:由于http请求是并行的,所以,看图

image-20201107215340515

上一秒所有的都没完成,进度一直卡在0%,下一秒所有的请求同时完成,瞬间进度100%,进度条俨然成了摆设

新思路:利用axios的onUploadProgress方法,创建一个长度为10的进度数组,然后更改对应的进度数据,利用computed计算属性返回10个分片进度的和

代码实现:

chunkList.map((item) => {
  const index = parseInt(item.fileName.split('-')[0])
  let form = new FormData()
  form.append("file", item.chunk, item.fileName)
  form.append("fileName", this.formData.identity)
  axiosArray.push(
    this.$http.post("/upload/part", form, {
      headers: { "Content-Type": "multipart/form-data" },
      onUploadProgress: (e) => {
        this.$set(this.percentage, index , e.loaded / e.total * 10 | 0)
      }
    })
  )
})

这里注意,computed并不能直接监听数组的响应式,需要使用$set这个API来设置元素的值,参数是(数组,下标,新的值)

上面计算的MD5值出了用于鉴别重复文件还可以用来实现秒传功能,两个文件的MD5相同那就表示是同一个文件,直接告诉用户上传完成,其实就是“欺骗用户”


前端小白