在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)

前言

之前写过一篇文章Vue+element-ui实现大文件分片上传,使用element-upload的
http-request这个参数去覆盖默认上传行为,达到大文件分片上传的效果,之前可能写的有点乱,并且那种方法有一些缺陷

  • 需要使用Vuex来存储正在上传中的xhr对象,在调用abort()取消上传的方法的时候,需要在组件外手动去遍历xhr数组,中断所有xhr请求,不然xhr都会继续上传,占用网速。
  • 传给后端的数据不能自定义,比如有的后端接口分片上传的文件下标叫index,有的叫chunkIndex,原先的方法将这些字段写到了组件内部,没做到通用性。
  • 分片大小和允许同时上传的分片数也是在组件内部写死了,没做到通用性

今天重新改造了一下,出了这篇组件改造,下面开始正文。

PS:我写这篇文章是2020年10月23日,element-ui的版本是2.13.2。

一、拷贝一份element-upload的源码。

源码在node_modules/element-ui/packages/upload中,把整个文件夹复制下来,放到你项目的components目录中,看下目录结构,我们主要改动到的是1,2,5三个文件

在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第1张图片
其中 6注册组件 的方法是我们引用element-ui时,给Vue.use(ElementUi)时使用的,我们改造组件的时候用不到,直接删了,顺便把src文件夹下的5个文件都移动到upload文件夹,然后删除src文件夹。
操作之后,你得到的应该如下图的5个文件。
在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第2张图片

二、删除一些原有的无用的依赖

我们直接把组件拷过来了,组件中有一些对element-ui的依赖,先删除掉。
1.index.vue文件,删除红框中的代码在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第3张图片
2.upload-list.vue文件,删除红框中的代码,把{{ t(‘el.upload.deleteTip’) }}改成‘按Delete删除’,locale和t()是多国语言,我们直接写成中文就好了,因为我们已经全局引入了element-ui了,所以ElProgress进度条这个组件在全局就已经存在了,所以也可以删掉。
在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第4张图片

三、新增分片上传功能

既然是改造成分片上传,组件就要新增下面2个参数

参数 说明 类型 默认值
chunk-size 每块切片的大小,单位 B,如 5MB 需要写成 1024*1024*5 Number 1024*1024*10
thread 线程,允许同一时间上传的分片数量 Number 3

index.vue中的props中添加

    chunkSize: {
      type: Number,
      default: 10 * 1024 * 1024,
    },
    thread: {
      type: Number,
      default: 3,
    },

在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第5张图片

并且将props中data的Object改成Function,
说明:改造前data是一个对象,直接传入需要的参数,改造之后,data是一个方法,每个分片上传都会调用这个方法,这个方法的返回值会作为当前分片上传时需要的参数,这么做的原因是:每个分片要携带的信息是不一样的,比如每个分片的hash,每个分片的下标等等。

参数 说明 类型
data 此方法要返回一个对象,这个对象内容就是每个分片携带的额外参数,参数option是一个对象,里面有chunkSize(当前分片的大小),chunkTotal(分片总数),chunkIndex(分片下标),chunkHash(分片hash),fileName(文件名),fileHash(文件hash),fileSize(文件大小)七个属性 function(options)

接着在render()方法中的uploadData的props中添加2个传给upload.vue的参数
在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第6张图片
接着修改upload.vue文件,在props中接收index.vue传进来的2个参数chunkSize和thread,同时把data的值Object改成Function
在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第7张图片
安装一下spark-md5,npm i spark-md5,用来计算文件hash用的
然后在upload.vue第4行左右写一个计算文件hash的方法

import SparkMD5 from 'spark-md5'
function getFileMD5(file) {
  return new Promise((resolve, reject) => {
    const spark = new SparkMD5.ArrayBuffer()
    const fileReader = new FileReader();
    fileReader.onload = (e) => {
      spark.append(e.target.result)
      resolve(spark.end())
    }
    fileReader.onerror = () => {
      reject("")
    }
    fileReader.readAsArrayBuffer(file)
  })
}

在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第8张图片
接着找到upload.vue文件的post()方法,在开始上传之前计算好文件hash等信息,传给ajax.js使用,红框部分就是新增的代码,用了await,记得在post前加上async。
在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第9张图片
接着修改ajax.js,把ajax.js中upload()方法替换成下面的代码,
这个方法修改前是单文件上传,逻辑是上传成功就调用option.onSuccess()回调,上传失败就调用onError()回调,上传过程中调用option.onProgress()更新上传进度条
现在改成分片上传,逻辑改成了,一个sendRequest()队列,并且通过thread来控制并发数,在全部分片上传完成之后才调用option.onSuccess()回调,只要有一个分片上传失败就调用option.onError(),并且修改了进度条option.onProgress()的计算方式
PS:这里有优化空间,队列某个分片上传失败,可以新增重试机制,重试2-3次失败之后再调用onError

export default function upload(option) {
  if (typeof XMLHttpRequest === 'undefined') {
    return;
  }
  const action = option.action // 文件上传上传路径
  const percentage = [] // 文件上传进度的数组,单项就是一个分片的进度
  //将Blob转为上传时需要的FormData格式
  const formDataList = option.chunkList.map((item, index) => {
    const formData = new FormData()
    // 额外加入组件外外面传入的data数据
    const md5 = option.chunkHashList[index]
    const chunkOption = {
      chunkSize: item.size,//当前分片大小
      chunkTotal: Math.ceil(option.file.size / option.chunkSize),// 所有切片数量
      chunkIndex: index,// 当前切片下标
      chunkHash: md5,// 当前切片hash
      fileName: option.file.name,// 文件名
      fileHash: option.fileHash,// 整个文件hash
      fileSize: option.file.size, // 总文件大小
    }
    if (option.data) {
      const data = option.data(chunkOption)
      //data是个方法,遍历data的返回值,让formData携带data中的参数
      if (data) {
        Object.keys(data).forEach(key => {
          formData.append(key, data[key])
        })
      }
    }
    formData.append(option.filename, item) // 文件的Blob
    return formData
  })

  // 更新上传进度条的方法
  const updataPercentage = (e) => {
    let loaded = 0// 当前已经上传文件的总大小
    percentage.forEach(item => {
      loaded += item
    })
    e.percent = loaded / option.file.size * 100
    option.onProgress(e)
  }
  const xhrList = [] // 所有的xhr请求
  function sendRequest(formDataList, limit) {
    let counter = 0 //上传成功的数量
    let index = 0 //当前上传文件的下标
    let isStop = false 
    const len = formDataList.length
    const start = async () => {
      if (isStop) {
        return
      }
      const item = formDataList.shift()
      if (item) {
        const chunkIndex = index++
        const xhr = new XMLHttpRequest()
        // 分片上传失败回调
        xhr.onerror = function error() {
          isStop = true
          option.onerror(getError(action, option, xhr))
        }
        // 分片上传成功回调
        xhr.onload = function onload() {
          if (xhr.status < 200 || xhr.status >= 300) {
            isStop = true
            option.onerror(getError(action, option, xhr))
          }
          // 最后一个上传完成
          if (counter === len - 1) {
            const result = xhrList.map(item => getBody(item))
            option.onSuccess(result)
          } else {
            counter++
            start()
          }
        }
        // 上传中的时候更新进度条
        if (xhr.upload) {
          xhr.upload.onprogress = function progress(e) {
            if (e.total > 0) {
              e.percent = e.loaded / e.total * 100
            }
            percentage[chunkIndex] = e.loaded
            updataPercentage(e)
          }
        }
        xhr.open('post', action, true)
        if (option.withCredentials && 'withCredentials' in xhr) {
          xhr.withCredentials = true
        }
        const headers = option.headers || {}
        // 添加请求头
        for (const item in headers) {
          if (Object.prototype.hasOwnProperty.call(headers, item) && headers[item] !== null) {
            xhr.setRequestHeader(item, headers[item])
          }
        }
        // 文件开始上传,并把xhr对象存入xhrList中
        xhr.send(item)
        xhrList.push(xhr)
      }
    }
    while (limit > 0) {
      setTimeout(() => {
        start()
      }, Math.random() * 1000)
      limit -= 1
    }
  }

  try {
    sendRequest(formDataList, option.thread)
    return xhrList
  } catch (error) {
    option.onError(error)
  }
}

改到这里就已经八九不离十了,还有最后一步,原先的ajax.js返回值是一个xhr,但是我们改造完之后返回值是一个xhr的数组集合,所以在取消上传的时候,要做一些修改,找到upload.vue文件的abort()方法,把里面2处reqs[uid].abort();改成红框部分的代码即可,到这里就大功告成了。
在Vue项目中创建自己的大文件分片上传组件(基于element-upload改造)_第10张图片

四、码云地址

https://gitee.com/GaoWeiQiang1996/element-chunk-upload

五、写在最后

on-success回调函数中的response参数也会有一些变化,以前是一个请求,现在有多少个分片就变成了有多少个请求的数组

其实可以添加一个开关来控制是否开启分片上传,后来想想,如果不用分片上传,把chunkSize设置成1024*1024*1024*1024,就和关闭分片上传没什么区别了,所以就没加。

其实还可以加秒传,断点续传等,在post()方法中我们就已经拿到文件hash和分片hash了,这时候去请求后端,拿回已经上传和文件hash,过滤掉,就能实现秒传断点续传了。

你可能感兴趣的:(Element-ui,Vue,vue,upload,elementui)