1. 前言
文件上傳 小文件(圖片、文檔、視頻)上傳可以直接使用很多ui框架封裝的上傳組件,或者自己寫一個(gè)input 上傳,利用FormData 對(duì)象提交文件數(shù)據(jù),后端使用spring提供的MultipartFile進(jìn)行文件的接收,然后寫入即可。但是對(duì)于比較大的文件,比如上傳2G左右的文件(http上傳),就需要將文件分片上傳(file.slice()),否則中間http長(zhǎng)時(shí)間連接可能會(huì)斷掉。
分片上傳 分片上傳,就是將所要上傳的文件,按照一定的大小,將整個(gè)文件分隔成多個(gè)數(shù)據(jù)塊(我們稱之為Part)來(lái)進(jìn)行分別上傳,上傳完之后再由服務(wù)端對(duì)所有上傳的文件進(jìn)行匯總整合成原始的文件。
秒傳 通俗的說(shuō),你把要上傳的東西上傳,服務(wù)器會(huì)先做MD5校驗(yàn),如果服務(wù)器上有一樣的東西,它就直接給你個(gè)新地址,其實(shí)你下載的都是服務(wù)器上的同一個(gè)文件,想要不秒傳,其實(shí)只要讓MD5改變,就是對(duì)文件本身做一下修改(改名字不行),例如一個(gè)文本文件,你多加幾個(gè)字,MD5就變了,就不會(huì)秒傳了.
斷點(diǎn)續(xù)傳 斷點(diǎn)續(xù)傳是在下載或上傳時(shí),將下載或上傳任務(wù)(一個(gè)文件或一個(gè)壓縮包)人為的劃分為幾個(gè)部分,每一個(gè)部分采用一個(gè)線程進(jìn)行上傳或下載,如果碰到網(wǎng)絡(luò)故障,可以從已經(jīng)上傳或下載的部分開(kāi)始繼續(xù)上傳或者下載未完成的部分,而沒(méi)有必要從頭開(kāi)始上傳或者下載。本文的斷點(diǎn)續(xù)傳主要是針對(duì)斷點(diǎn)上傳場(chǎng)景。
關(guān)于vue-simple-uploader
vue-simple-uploader是基于 simple-uploader.js 封裝的vue上傳插件。它的優(yōu)點(diǎn)包括且不限于以下幾種:
- 支持文件、多文件、文件夾上傳;支持拖拽文件、文件夾上傳
- 可暫停、繼續(xù)上傳
- 錯(cuò)誤處理
- 支持“秒傳”,通過(guò)文件判斷服務(wù)端是否已存在從而實(shí)現(xiàn)“秒傳”
- 分塊上傳
- 支持進(jìn)度、預(yù)估剩余時(shí)間、出錯(cuò)自動(dòng)重試、重傳等操作
大文件上傳流程
- 前端對(duì)文件進(jìn)行MD5加密,并且將文件按一定的規(guī)則分片;
- vue-simple-uploader先會(huì)發(fā)送get請(qǐng)求校驗(yàn)分片數(shù)據(jù)在服務(wù)端是否完整,如果完整則進(jìn)行秒傳,如果不完整或者無(wú)數(shù)據(jù),則進(jìn)行分片上傳。
- 后臺(tái)校驗(yàn)MD5值,根據(jù)上傳的序號(hào)和分片大小計(jì)算相應(yīng)的開(kāi)始位置并寫入該分片數(shù)據(jù)到文件中。
建議先讀一遍simple-uploader.js的文檔,然后再讀一下vue-simple-uploader的文檔,了解一下各個(gè)參數(shù)的作用是什么
vue-simple-uploader文檔
simple-uploader.js文檔
安裝
npm install vue-simple-uploader --save
使用:
// 在main.js中:
import uploader from 'vue-simple-uploader'
Vue.use(uploader)
相關(guān)概念
- chunkNumber: 當(dāng)前塊的次序,第一個(gè)塊是 1,注意不是從 0 開(kāi)始的。
- totalChunks: 文件被分成塊的總數(shù)。
- chunkSize: 分塊大小,根據(jù) totalSize 和這個(gè)值可以計(jì)算出總共的塊數(shù)。注意最后一塊的大小可能會(huì)比這個(gè)要大。
- currentChunkSize: 當(dāng)前塊的大小,實(shí)際大小。
- totalSize: 文件總大小。
- identifier: 這個(gè)就是MD5值,每個(gè)文件的唯一標(biāo)示。
- filename: 文件名
基于vue-simple-uploader封裝全局上傳組件
代碼參考:
git代碼
- 點(diǎn)擊按鈕,觸發(fā)文件上傳操作;
- 選擇文件后,將上傳的窗口展示出來(lái),開(kāi)始md5的計(jì)算工作
onFileAdded(file) {
this.panelShow = true
Bus.$emit('fileAdded')
// 將額外的參數(shù)賦值到每個(gè)文件上,以不同文件使用不同params的需求
file.params = this.params
// 計(jì)算MD5,完成后開(kāi)始上傳操作
this.computeMD5(file).then((result) => this.startUpload(result))
},
具體的MD5計(jì)算方法,會(huì)在下面講,這里先簡(jiǎn)單引出。
上傳過(guò)程中,會(huì)不斷觸發(fā)file-progress上傳進(jìn)度的回調(diào)
// 文件進(jìn)度的回調(diào)
onFileProgress(rootFile, file, chunk) {
console.log(`上傳中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
},
- 文件上傳成功后
文件上傳成功后,在“上傳完成”的回調(diào)中,通過(guò)服務(wù)端返回的needMerge字段,來(lái)判斷是否需要再發(fā)送合并分片的請(qǐng)求,
如果這個(gè)字段為true,則需要給后臺(tái)發(fā)一個(gè)請(qǐng)求合并的ajax請(qǐng)求,否則直接上傳成功。
注意:這里的needMerge是和后臺(tái)商議決定的字段名
onFileSuccess(rootFile, file, response, chunk) {
let res = JSON.parse(response);
// 服務(wù)器自定義的錯(cuò)誤,這種錯(cuò)誤是Uploader無(wú)法攔截的
if (!res.result) {
this.$message({ message: res.message, type: 'error' });
return
}
// 如果服務(wù)端返回需要合并
if (res.needMerge) {
api.mergeSimpleUpload({
tempName: res.tempName,
fileName: file.name,
...file.params,
}).then(data => {
// 文件合并成功
Bus.$emit('fileSuccess', data);
}).catch(e => {});
// 不需要合并
} else {
Bus.$emit('fileSuccess', res);
console.log('上傳成功');
}
},
onFileError(rootFile, file, response, chunk) {
console.log(error)
},
- 文件分片
vue-simple-uploader自動(dòng)將文件進(jìn)行分片,在options的chunkSize中可以設(shè)置每個(gè)分片的大小。對(duì)于大文件來(lái)說(shuō),會(huì)發(fā)送多個(gè)請(qǐng)求,在設(shè)置testChunks為true后(在插件中默認(rèn)就是true),會(huì)發(fā)送與服務(wù)器進(jìn)行分片校驗(yàn)的請(qǐng)求,第一個(gè)get請(qǐng)求就是該請(qǐng)求;后面的每一個(gè)post請(qǐng)求都是上傳分片的請(qǐng)求。
看一下發(fā)送給服務(wù)端的參數(shù),其中chunkNumber表示當(dāng)前是第幾個(gè)分片,totalChunks代表所有的分片數(shù),這兩個(gè)參數(shù)都是都是插件根據(jù)你設(shè)置的chunkSize來(lái)計(jì)算的。
需要注意的就是在最后文件上傳成功的事件中,通過(guò)后臺(tái)返回的字段,來(lái)判斷是否要再給后臺(tái)發(fā)送一個(gè)文件合并的請(qǐng)求。
- MD5的計(jì)算過(guò)程
斷點(diǎn)續(xù)傳及秒傳的基礎(chǔ)是要計(jì)算文件的MD5,這是文件的唯一標(biāo)識(shí),然后服務(wù)器根據(jù)MD5進(jìn)行判斷,是進(jìn)行秒傳還是斷點(diǎn)續(xù)傳。在file-added事件之后,就計(jì)算MD5,我們最終的目的是將計(jì)算出來(lái)的MD5加到參數(shù)里傳給后臺(tái),然后繼續(xù)文件上傳的操作,詳細(xì)的思路步驟是:
1)把uploader組件的autoStart設(shè)為false,即選擇文件后不會(huì)自動(dòng)開(kāi)始上傳
2)先通過(guò) file.pause()暫停文件,然后通過(guò)H5的FileReader接口讀取文件
3)將異步讀取文件的結(jié)果進(jìn)行MD5,這里我用的加密工具是spark-md5,你可以通過(guò)npm install spark-md5 --save來(lái)安裝,也可以使用其他MD5加密工具。
4)file有個(gè)屬性是uniqueIdentifier,代表文件唯一標(biāo)示,我們把計(jì)算出來(lái)的MD5賦值給這個(gè)屬性 file.uniqueIdentifier = md5,這就實(shí)現(xiàn)了我們最終的目的。
5)通過(guò)file.resume()開(kāi)始/繼續(xù)文件上傳。
/**
* 計(jì)算md5值,以實(shí)現(xiàn)斷點(diǎn)續(xù)傳及秒傳
* @param file
* @returns Promise
*/
computeMD5(file) {
let fileReader = new FileReader()
let time = new Date().getTime()
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
let currentChunk = 0
const chunkSize = 10 * 1024 * 1000
let chunks = Math.ceil(file.size / chunkSize)
let spark = new SparkMD5.ArrayBuffer()
// 文件狀態(tài)設(shè)為"計(jì)算MD5"
this.statusSet(file.id, 'md5')
file.pause()
// 計(jì)算MD5時(shí)隱藏”開(kāi)始“按鈕
this.$nextTick(() => {
document.querySelector(`.file-${file.id} .uploader-file-resume`).style.display = 'none'
})
loadNext()
return new Promise((resolve, reject) => {
fileReader.onload = (e) => {
spark.append(e.target.result)
if (currentChunk < chunks) {
currentChunk++
loadNext()
// 實(shí)時(shí)展示MD5的計(jì)算進(jìn)度
this.$nextTick(() => {
const md5ProgressText ='校驗(yàn)MD5 '+ ((currentChunk/chunks)*100).toFixed(0)+'%'
document.querySelector(`.custom-status-${file.id}`).innerText = md5ProgressText
})
} else {
let md5 = spark.end()
// md5計(jì)算完畢
resolve({md5, file})
console.log(
`MD5計(jì)算完畢:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用時(shí):${
new Date().getTime() - time
} ms`
)
}
}
fileReader.onerror = function () {
this.error(`文件${file.name}讀取出錯(cuò),請(qǐng)檢查該文件`)
file.cancel()
reject()
}
})
function loadNext() {
let start = currentChunk * chunkSize
let end = start + chunkSize >= file.size ? file.size : start + chunkSize
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
}
},
// md5計(jì)算完畢,開(kāi)始上傳
startUpload({md5, file}) {
file.uniqueIdentifier = md5
file.resume()
this.statusRemove(file.id)
},
給file的uniqueIdentifier 屬性賦值后,請(qǐng)求中的identifier即是我們計(jì)算出來(lái)的MD5文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-462487.html
- 秒傳及斷點(diǎn)續(xù)傳
在計(jì)算完MD5后,我們就能談斷點(diǎn)續(xù)傳及秒傳的概念了。
服務(wù)器根據(jù)前端傳過(guò)來(lái)的MD5去判斷是否可以進(jìn)行秒傳或斷點(diǎn)續(xù)傳:
1)服務(wù)器發(fā)現(xiàn)文件已經(jīng)完全上傳成功,則直接返回秒傳的標(biāo)識(shí)。
2)服務(wù)器發(fā)現(xiàn)文件上傳過(guò)分片信息,則返回這些分片信息,告訴前端繼續(xù)上傳,即斷點(diǎn)續(xù)傳。
3)對(duì)于前端來(lái)說(shuō)在每次上傳過(guò)程的最開(kāi)始,vue-simple-uploader會(huì)發(fā)送一個(gè)get請(qǐng)求,來(lái)問(wèn)服務(wù)器我哪些分片已經(jīng)上傳過(guò)了,
這個(gè)請(qǐng)求返回的結(jié)果也有幾種可能:
a. 如果是秒傳,在請(qǐng)求結(jié)果中會(huì)有相應(yīng)的標(biāo)識(shí),比如我這里是skipUpload為true,且返回了url,代表服務(wù)器告訴我們這個(gè)文件已經(jīng)有了,我直接把url給你,你不用再傳了,這就是秒傳。
圖1,秒傳狀態(tài)下返回值
b. 如果后臺(tái)返回了分片信息,這是斷點(diǎn)續(xù)傳。如圖,返回的數(shù)據(jù)中有個(gè)uploaded的字段,代表這些分片是已經(jīng)上傳過(guò)的了,插件會(huì)自動(dòng)跳過(guò)這些分片的上傳。
7.2 前端做分片檢驗(yàn):checkChunkUploadedByResponse
前面講的是概念,現(xiàn)在說(shuō)一說(shuō)前端在拿到這些返回值之后怎么處理。
插件自己是不會(huì)判斷哪個(gè)需要跳過(guò)的,在代碼中由options中的checkChunkUploadedByResponse控制,它會(huì)根據(jù) XHR 響應(yīng)內(nèi)容檢測(cè)每個(gè)塊是否上傳成功了,成功的分片直接跳過(guò)上傳
你要在這個(gè)函數(shù)中進(jìn)行處理,可以跳過(guò)的情況下返回true即可。
checkChunkUploadedByResponse: function (chunk, message) {
let objMessage = JSON.parse(message);
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
// 注:skipUpload 和 uploaded 是和后臺(tái)商議的字段,要按照后臺(tái)實(shí)際返回的字段名來(lái)。
整理了下vue-simple-uploader 常見(jiàn)的問(wèn)題:
vue-simple-uploader 常見(jiàn)問(wèn)題整理文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-462487.html
到了這里,關(guān)于基于vue-simple-uploader封裝文件分片上傳、秒傳及斷點(diǎn)續(xù)傳的全局上傳的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!