天天看點

ElementUI el-upload 斷點續傳檔案

官方的

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[p])
      }
      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[p])
      }
      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>
           

轉載請注明出處蟹蟹

繼續閱讀