官方的
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>
轉載請注明出處蟹蟹