ElementUI el-upload 断点续传文件

2022年6月7日13:18:51

官方的Upload 组件从文档和所有demo来看,均是选中文件直接上传,但是业务系统有大文件上传的需求,所以要用这个组件封装一个断点续传的功能。

从官方给出的文档看到有个http-request 覆盖默认的上传行为,可以自定义上传的实现 似乎能满足要求,那就开撸。

确定需求:最大支持2GB的任意文件上传,小于100M直接上传,大于100M的时候分块上传,并且要支持断点续传。

我拿了官方的一个demo

 <el-upload
      drag
      multiple
      :http-request="checkedFile"
      action="/"
    >
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
    </el-upload>

可拖动上传,比较高大上一点。

http-request 方法定义之后,文件上传会先走这个方法,传入一个参数

options = {
	headers: this.headers,
	withCredentials: this.withCredentials,
	file: rawFile,
	data: this.data,
	filename: this.name,
	action: this.action,
	onProgress: e => {
	  this.onProgress(e, rawFile);
	},
	onSuccess: res => {
	  this.onSuccess(res, rawFile);
	  delete this.reqs[uid];
	},
	onError: err => {
	  this.onError(err, rawFile);
	  delete this.reqs[uid];
	}
}

该参数就是组件的参数集合,同时,如果定义了这个方法,组件的submit方法就会被拦截掉(注意别在这个方法里面调用组件的submit 方法,会造成死循环),在这个方法里面我就可以搞我想搞的事情了。

说一下要注意的:
使用这个断点续传方法一定要先和服务端协调好,看他们怎么处理的,比如我这里就是按照文件分块后按照序号和文件id等信息跟服务端建立联系,服务端从接收到第一块文件的请求开始就会检测该文件是否已经存在已接收的文件块,然后再返回续传的块的序号,最终再调用接口校验文件完不完整。

如果使用mock来模拟接口的话,onUploadProgress是无效的,因为mock会重新声明一个XMLHttpRequest,不会继承onUploadProgress

以上,就是el-upload组件的大文件分块上传的改造方案,目前还很粗糙,甚至还没过测试,如有问题会持续更新

20180726
看源码的时候发现http-request 这个传入的回调函数应该返回一个Promise

  const req = this.httpRequest(options);
  this.reqs[uid] = req;
  if (req && req.then) {
    req.then(options.onSuccess, options.onError);
  }

然后组件自己会做成功和错误的处理,但是我同时又注意到了组件是有删除文件的功能的,那我请求自己实现的话,这功能岂不是没法用?果然我一点X,立马报了一个reqs[uid].abort is not a function ,果然如此,我返回了一个最普通的Promise,当然没有abort方法了(是原生XMLHttpRequest 对象的方法)
我在返回的Promise动了一下手脚

const prom = new Promise((resolve, reject) => {})
prom.abort = () => {}
return prom

这三句话的意思就:大爷我给您跪了,别报错……
接下来就在组件的钩子函数before-remove来处理删除文件的功能。
axios截断请求可以传入一个cancelToken的值来返回一个cancel function,这部分就在data里面添加一个请求队列的参数,再把文件id和相关请求的截断方法push进去就可以了,改造后的postFile方法如下

 postFile (param, onProgress) {
      const formData = new FormData()
      for (let p in param) {
        formData.append(p, param

) } const { requestCancelQueue } = this const config = { cancelToken: new axios.CancelToken(function executor (cancel) { if (requestCancelQueue[param.uid]) { requestCancelQueue[param.uid]() delete requestCancelQueue[param.uid] } requestCancelQueue[param.uid] = cancel }), onUploadProgress: e => { e.percent = Number(((e.loaded / e.total) * (1 / (param.chunks || 1)) * 100).toFixed(2)) onProgress(e) } } return axios.post('/upload', formData, config).then(rs => rs.data.data) }

然后before-remove钩子的处理就只需要调用就OK了

	removeFile (file) {
      this.requestCancelQueue[file.uid]()
      delete this.requestCancelQueue[file.uid]
      return false
    }

最后贴上完整的组件源码,直接拷贝粘贴可用(依赖element-ui、axios)

<template>
  <el-upload
    drag
    multiple
    :auto-upload="true"
    :http-request="checkedFile"
    :before-remove="removeFile"
    :limit="10"
    action="/"
  >
    <i class="el-icon-upload"></i>
    <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
  </el-upload>
</template>
<script>
import axios from 'axios'
export default {
  data () {
    return {
      maxSize: 5 * 1024 * 1024 * 1024, // 上传最大文件限制
      multiUploadSize: 500 * 1024 * 1024, // 大于这个大小的文件使用分块上传(后端可以支持断点续传)
      eachSize: 500 * 1024 * 1024, // 每块文件大小
      requestCancelQueue: [], // 请求方法队列(调用取消上传
    }
  },
  mounted () {
  },
  methods: {
    async checkedFile (options) {
      const { maxSize, multiUploadSize, getSize, splitUpload, singleUpload } = this
      const { file, onProgress, onSuccess, onError } = options
      if (file.size > maxSize) {
        return this.$message({
          message: `您选择的文件大于${getSize(maxSize)}`,
          type: 'error'
        })
      }
      const uploadFunc = file.size > multiUploadSize ? splitUpload : singleUpload
      try {
        await uploadFunc(file, onProgress)
        this.$message({
          message: '上传成功',
          type: 'success'
        })
        onSuccess()
      } catch (e) {
        console.error(e)
        this.$message({
          message: e.message,
          type: 'error'
        })
        onError()
      }
      const prom = new Promise((resolve, reject) => {})
      prom.abort = () => {}
      return prom
    },
    // 格式化文件大小显示文字
    getSize (size) {
      return size > 1024
        ? size / 1024 > 1024
          ? size / (1024 * 1024) > 1024
            ? (size / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
            : (size / (1024 * 1024)).toFixed(2) + 'MB'
          : (size / 1024).toFixed(2) + 'KB'
        : (size).toFixed(2) + 'B'
    },
    // 单文件直接上传
    singleUpload (file, onProgress) {
      return this.postFile({ file, uid: file.uid, fileName: file.fileName }, onProgress)
    },
    // 大文件分块上传
    splitUpload (file, onProgress) {
      return new Promise(async (resolve, reject) => {
        try {
          const { eachSize } = this
          const chunks = Math.ceil(file.size / eachSize)
          const fileChunks = await this.splitFile(file, eachSize, chunks)
          let currentChunk = 0
          for (let i = 0; i < fileChunks.length; i++) {
            // 服务端检测已经上传到第currentChunk块了,那就直接跳到第currentChunk块,实现断点续传
            console.log(currentChunk, i)
            if (Number(currentChunk) === i) {
              // 每块上传完后则返回需要提交的下一块的index
              currentChunk = await this.postFile({
                chunked: true,
                chunk: i,
                chunks,
                eachSize,
                fileName: file.name,
                fullSize: file.size,
                uid: file.uid,
                file: fileChunks[i]
              }, onProgress)
            }
          }
          const isValidate = await this.validateFile({
            chunks: fileChunks.length,
            fileName: file.name,
            fullSize: file.size,
            uid: file.uid
          })
          if (!isValidate) {
            throw new Error('文件校验异常')
          }
          resolve()
        } catch (e) {
          reject(e)
        }
      })
    },
    // 文件分块,利用Array.prototype.slice方法
    splitFile (file, eachSize, chunks) {
      return new Promise((resolve, reject) => {
        try {
          setTimeout(() => {
            const fileChunk = []
            for (let chunk = 0; chunks > 0; chunks--) {
              fileChunk.push(file.slice(chunk, chunk + eachSize))
              chunk += eachSize
            }
            resolve(fileChunk)
          }, 0)
        } catch (e) {
          console.error(e)
          reject(new Error('文件切块发生错误'))
        }
      })
    },
    removeFile (file) {
      this.requestCancelQueue[file.uid]()
      delete this.requestCancelQueue[file.uid]
      return true
    },
    // 提交文件方法,将参数转换为FormData, 然后通过axios发起请求
    postFile (param, onProgress) {
      const formData = new FormData()
      for (let p in param) {
        formData.append(p, param

) } const { requestCancelQueue } = this const config = { cancelToken: new axios.CancelToken(function executor (cancel) { if (requestCancelQueue[param.uid]) { requestCancelQueue[param.uid]() delete requestCancelQueue[param.uid] } requestCancelQueue[param.uid] = cancel }), onUploadProgress: e => { if (param.chunked) { e.percent = Number(((((param.chunk * (param.eachSize - 1)) + (e.loaded)) / param.fullSize) * 100).toFixed(2)) } else { e.percent = Number(((e.loaded / e.total) * 100).toFixed(2)) } onProgress(e) } } return axios.post('http://localhost:8888', formData, config).then(rs => rs.data) }, // 文件校验方法 validateFile (file) { return axios.post('http://localhost:8888/validateFile', file).then(rs => rs.data) } } } </script>

转载请注明出处蟹蟹

  • 作者:前端弓箭手
  • 原文链接:https://blog.csdn.net/qq_19694913/article/details/81208049
    更新时间:2022年6月7日13:18:51 ,共 5661 字。