dio 上传文件报错_Vue+Element UI实现断点续传、分片上传、秒传
作者:Pseudo
轉發鏈接:https://segmentfault.com/a/1190000023434864
凡是要知其然知其所以然
文件上傳相信很多朋友都有遇到過,那或許你也遇到過當上傳大文件時,上傳時間較長,且經常失敗的困擾,并且失敗后,又得重新上傳很是煩人。那我們先了解下失敗的原因吧!
前面小編也整理過關于文件上傳的詳細原理和文件上傳技巧:
手把手教你前端的各種文件上傳攻略和大文件斷點續傳
一文了解文件上傳全過程(1.8w字深度解析)「前端進階必備」
據我了解大概有以下原因:
基于以上原因,聰明的人們就想到了,將文件拆分多個小文件,依次上傳,不就解決以上1,2問題嘛,這便是分片上傳。 網絡波動這個實在不可控,也許一陣大風刮來,就斷網了呢。那這樣好了,既然斷網無法控制,那我可以控制只上傳以經上傳的文件內容,不就好了,這樣大大加快了重新上傳的速度。所以便有了“斷點續傳”一說。此時,人群中有人插了一嘴,有些文件我已經上傳一遍了,為啥還要在上傳,能不能不浪費我流量和時間。喔...這個嘛,簡單,每次上傳時判斷下是否存在這個文件,若存在就不重新上傳便可,于是又有了“秒傳”一說。從此這"三兄弟" 便自行CP,統治了整個文件界?!?/p>
注意文中的代碼并非實際代碼,請移步至github查看最新代碼
github: https://github.com/pseudo-god/vue-simple-upload
分片上傳
HTML
原生INPUT樣式較丑,這里通過樣式疊加的方式,放一個Button.
選擇文件 上傳暫停恢復清空 ({ container: { files: null }, tempFilesArr: [], // 存儲files信息 cancels: [], // 存儲要取消的請求 tempThreads: 3, // 默認狀態 status: Status.wait }),一個稍微好看的UI就出來了。
選擇文件
選擇文件過程中,需要對外暴露出幾個鉤子,熟悉elementUi的同學應該很眼熟,這幾個鉤子基本與其一致。onExceed:文件超出個數限制時的鉤子、beforeUpload:文件上傳之前
fileIndex 這個很重要,因為是多文件上傳,所以定位當前正在被上傳的文件就很重要,基本都靠它
handleFileChange(e) { const files = e.target.files; if (!files) return; Object.assign(this.$data, this.$options.data()); // 重置data所有數據 fileIndex = 0; // 重置文件下標 this.container.files = files; // 判斷文件選擇的個數 if (this.limit && this.container.files.length > this.limit) { this.onExceed && this.onExceed(files); return; } // 因filelist不可編輯,故拷貝filelist 對象 var index = 0; // 所選文件的下標,主要用于剔除文件后,原文件list與臨時文件list不對應的情況 for (const key in this.container.files) { if (this.container.files.hasOwnProperty(key)) { const file = this.container.files[key]; if (this.beforeUpload) { const before = this.beforeUpload(file); if (before) { this.pushTempFile(file, index); } } if (!this.beforeUpload) { this.pushTempFile(file, index); } index++; } }},// 存入 tempFilesArr,為了上面的鉤子,所以將代碼做了拆分pushTempFile(file, index) { // 額外的初始值 const obj = { status: fileStatus.wait, chunkList: [], uploadProgress: 0, hashProgress: 0, index }; for (const k in file) { obj[k] = file[k]; } console.log('pushTempFile -> obj', obj); this.tempFilesArr.push(obj);}分片上傳
創建切片,循環分解文件即可 createFileChunk(file, size = chunkSize) { const fileChunkList = []; var count = 0; while (count < file.size) { fileChunkList.push({ file: file.slice(count, count + size) }); count += size; } return fileChunkList;}循環創建切片,既然咱們做的是多文件,所以這里就有循環去處理,依次創建文件切片,及切片的上傳。async handleUpload(resume) { if (!this.container.files) return; this.status = Status.uploading; const filesArr = this.container.files; var tempFilesArr = this.tempFilesArr; for (let i = 0; i < tempFilesArr.length; i++) { fileIndex = i; //創建切片 const fileChunkList = this.createFileChunk( filesArr[tempFilesArr[i].index] ); tempFilesArr[i].fileHash ='xxxx'; // 先不用看這個,后面會講,占個位置 tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({ fileHash: tempFilesArr[i].hash, fileName: tempFilesArr[i].name, index, hash: tempFilesArr[i].hash + '-' + index, chunk: file, size: file.size, uploaded: false, progress: 0, // 每個塊的上傳進度 status: 'wait' // 上傳狀態,用作進度狀態顯示 })); //上傳切片 await this.uploadChunks(this.tempFilesArr[i]); }}上傳切片,這個里需要考慮的問題較多,也算是核心吧,uploadChunks方法只負責構造傳遞給后端的數據,核心上傳功能放到sendRequest方法中 async uploadChunks(data) { var chunkData = data.chunkList; const requestDataList = chunkData .map(({ fileHash, chunk, fileName, index }) => { const formData = new FormData(); formData.append('md5', fileHash); formData.append('file', chunk); formData.append('fileName', index); // 文件名使用切片的下標 return { formData, index, fileName }; }); try { await this.sendRequest(requestDataList, chunkData); } catch (error) { // 上傳有被reject的 this.$message.error('親 上傳失敗了,考慮重試下呦' + error); return; } // 合并切片 const isUpload = chunkData.some(item => item.uploaded === false); console.log('created -> isUpload', isUpload); if (isUpload) { alert('存在失敗的切片'); } else { // 執行合并 await this.mergeRequest(data); }}sendReques。上傳這是最重要的地方,也是容易失敗的地方,假設有10個分片,那我們若是直接發10個請求的話,很容易達到瀏覽器的瓶頸,所以需要對請求進行并發處理。并發處理:這里我使用for循環控制并發的初始并發數,然后在 handler 函數里調用自己,這樣就控制了并發。在handler中,通過數組API.shift模擬隊列的效果,來上傳切片。重點: retryArr 數組存儲每個切片文件請求的重試次數,做累加。比如[1,0,2],就是第0個文件切片報錯1次,第2個報錯2次。為保證能與文件做對應,const index = formInfo.index; 我們直接從數據中拿之前定義好的index。 若失敗后,將失敗的請求重新加入隊列即可。關于并發及重試我寫了一個小Demo,若不理解可以自己在研究下,文件地址:https://github.com/pseudo-god/vue-simple-upload/blob/master/src/utils/sendRequest-domo.js, 重試代碼好像被我弄丟了,大家要是有需求,我再補吧! // 并發處理sendRequest(forms, chunkData) { var finished = 0; const total = forms.length; const that = this; const retryArr = []; // 數組存儲每個文件hash請求的重試次數,做累加 比如[1,0,2],就是第0個文件切片報錯1次,第2個報錯2次 return new Promise((resolve, reject) => { const handler = () => { if (forms.length) { // 出棧 const formInfo = forms.shift(); const formData = formInfo.formData; const index = formInfo.index; instance.post('fileChunk', formData, { onUploadProgress: that.createProgresshandler(chunkData[index]), cancelToken: new CancelToken(c => this.cancels.push(c)), timeout: 0 }).then(res => { console.log('handler -> res', res); // 更改狀態 chunkData[index].uploaded = true; chunkData[index].status = 'success'; finished++; handler(); }) .catch(e => { // 若暫停,則禁止重試 if (this.status === Status.pause) return; if (typeof retryArr[index] !== 'number') { retryArr[index] = 0; } // 更新狀態 chunkData[index].status = 'warning'; // 累加錯誤次數 retryArr[index]++; // 重試3次 if (retryArr[index] >= this.chunkRetry) { return reject('重試失敗', retryArr); } this.tempThreads++; // 釋放當前占用的通道 // 將失敗的重新加入隊列 forms.push(formInfo); handler(); }); } if (finished >= total) { resolve('done'); } }; // 控制并發 for (let i = 0; i < this.tempThreads; i++) { handler(); } });}切片的上傳進度,通過axios的onUploadProgress事件,結合createProgresshandler方法進行維護// 切片上傳進度createProgresshandler(item) { return p => { item.progress = parseInt(String((p.loaded / p.total) * 100)); this.fileProgress(); };}Hash計算
其實就是算一個文件的MD5值,MD5在整個項目中用到的地方也就幾點。
秒傳,需要通過MD5值判斷文件是否已存在。續傳:需要用到MD5作為key值,當唯一值使用。本項目主要使用worker處理,性能及速度都會有很大提升.
由于是多文件,所以HASH的計算進度也要體現在每個文件上,所以這里使用全局變量fileIndex來定位當前正在被上傳的文件
因使用worker,所以我們不能直接使用NPM包方式使用MD5。需要單獨去下載spark-md5.js文件,并引入
//hash.jsself.importScripts("/spark-md5.min.js"); // 導入腳本// 生成文件 hashself.onmessage = e => { const { fileChunkList } = e.data; const spark = new self.SparkMD5.ArrayBuffer(); let percentage = 0; let count = 0; const loadNext = index => { const reader = new FileReader(); reader.readAsArrayBuffer(fileChunkList[index].file); reader.onload = e => { count++; spark.append(e.target.result); if (count === fileChunkList.length) { self.postMessage({ percentage: 100, hash: spark.end() }); self.close(); } else { percentage += 100 / fileChunkList.length; self.postMessage({ percentage }); loadNext(count); } }; }; loadNext(0);};文件合并
當我們的切片全部上傳完畢后,就需要進行文件的合并,這里我們只需要請求接口即可
mergeRequest(data) { const obj = { md5: data.fileHash, fileName: data.name, fileChunkNum: data.chunkList.length }; instance.post('fileChunk/merge', obj, { timeout: 0 }) .then((res) => { this.$message.success('上傳成功'); }); }Done: 至此一個分片上傳的功能便已完成
斷點續傳
顧名思義,就是從那斷的就從那開始,明確思路就很簡單了。一般有2種方式,一種為服務器端返回,告知我從那開始,還有一種是瀏覽器端自行處理。2種方案各有優缺點。本項目使用第二種。
思路:已文件HASH為key值,每個切片上傳成功后,記錄下來便可。若需要續傳時,直接跳過記錄中已存在的便可。本項目將使用Localstorage進行存儲,這里我已提前封裝好addChunkStorage、getChunkStorage方法。
存儲在Stroage的數據
緩存處理
在切片上傳的axios成功回調中,存儲已上傳成功的切片
instance.post('fileChunk', formData, ) .then(res => { // 存儲已上傳的切片下標+ this.addChunkStorage(chunkData[index].fileHash, index); handler(); })在切片上傳前,先看下localstorage中是否存在已上傳的切片,并修改uploaded
async handleUpload(resume) {+ const getChunkStorage = this.getChunkStorage(tempFilesArr[i].hash); tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({+ uploaded: getChunkStorage && getChunkStorage.includes(index), // 標識:是否已完成上傳+ progress: getChunkStorage && getChunkStorage.includes(index) ? 100 : 0,+ status: getChunkStorage && getChunkStorage.includes(index)? 'success'+ : 'wait' // 上傳狀態,用作進度狀態顯示 })); }構造切片數據時,過濾掉uploaded為true的
async uploadChunks(data) { var chunkData = data.chunkList; const requestDataList = chunkData+ .filter(({ uploaded }) => !uploaded) .map(({ fileHash, chunk, fileName, index }) => { const formData = new FormData(); formData.append('md5', fileHash); formData.append('file', chunk); formData.append('fileName', index); // 文件名使用切片的下標 return { formData, index, fileName }; })}垃圾文件清理
隨著上傳文件的增多,相應的垃圾文件也會增多,比如有些時候上傳一半就不再繼續,或上傳失敗,碎片文件就會增多。解決方案我目前想了2種
前端在localstorage設置緩存時間,超過時間就發送請求通知后端清理碎片文件,同時前端也要清理緩存。前后端都約定好,每個緩存從生成開始,只能存儲12小時,12小時后自動清理以上2種方案似乎都有點問題,極有可能造成前后端因時間差,引發切片上傳異常的問題,后面想到合適的解決方案再來更新吧。
Done: 續傳到這里也就完成了。
秒傳
這算是最簡單的,只是聽起來很厲害的樣子。原理:計算整個文件的HASH,在執行上傳操作前,向服務端發送請求,傳遞MD5值,后端進行文件檢索。若服務器中已存在該文件,便不進行后續的任何操作,上傳也便直接結束。大家一看就明白
async handleUpload(resume) { if (!this.container.files) return; const filesArr = this.container.files; var tempFilesArr = this.tempFilesArr; for (let i = 0; i < tempFilesArr.length; i++) { const fileChunkList = this.createFileChunk( filesArr[tempFilesArr[i].index] ); // hash校驗,是否為秒傳+ tempFilesArr[i].hash = await this.calculateHash(fileChunkList);+ const verifyRes = await this.verifyUpload(+ tempFilesArr[i].name,+ tempFilesArr[i].hash+ );+ if (verifyRes.data.presence) {+ tempFilesArr[i].status = fileStatus.secondPass;+ tempFilesArr[i].uploadProgress = 100;+ } else { console.log('開始上傳切片文件----》', tempFilesArr[i].name); await this.uploadChunks(this.tempFilesArr[i]); } } } // 文件上傳之前的校驗: 校驗文件是否已存在 verifyUpload(fileName, fileHash) { return new Promise(resolve => { const obj = { md5: fileHash, fileName, ...this.uploadArguments //傳遞其他參數 }; instance .post('fileChunk/presence', obj) .then(res => { resolve(res.data); }) .catch(err => { console.log('verifyUpload -> err', err); }); }); }Done: 秒傳到這里也就完成了。
后端處理
文章好像有點長了,具體代碼邏輯就先不貼了,除非有人留言要求,嘻嘻,有時間再更新
Node版
請前往 https://github.com/pseudo-god... 查看
JAVA版
下周應該會更新處理
PHP版
1年多沒寫PHP了,抽空我會慢慢補上來
待完善
切片的大小:這個后面會做出動態計算的。需要根據當前所上傳文件的大小,自動計算合適的切片大小。避免出現切片過多的情況。文件追加:目前上傳文件過程中,不能繼續選擇文件加入隊列。(這個沒想好應該怎么處理。)更新記錄
組件已經運行一段時間了,期間也測試出幾個問題,本來以為沒BUG的,看起來BUG都挺嚴重
BUG-1:當同時上傳多個內容相同但是文件名稱不同的文件時,出現上傳失敗的問題。
預期結果:第一個上傳成功后,后面相同的文文件應該直接秒傳
實際結果:第一個上傳成功后,其余相同的文件都失敗,錯誤信息,塊數不對。
原因:當第一個文件塊上傳完畢后,便立即進行了下一個文件的循環,導致無法及時獲取文件是否已秒傳的狀態,從而導致失敗。
解決方案:在當前文件分片上傳完畢并且請求合并接口完畢后,再進行下一次循環。
將子方法都改為同步方式,mergeRequest 和 uploadChunks 方法
BUG-2: 當每次選擇相同的文件并觸發beforeUpload方法時,若第二次也選擇了相同的文件,beforeUpload方法失效,從而導致整個流程失效。
原因:之前每次選擇文件時,沒有清空上次所選input文件的數據,相同數據的情況下,是不會觸發input的change事件。
解決方案:每次點擊input時,清空數據即可。我順帶優化了下其他的代碼,具體看提交記錄吧。
重寫了暫停和恢復的功能,實際上,主要是增加了暫停和恢復的狀態
之前的處理邏輯太簡單粗暴,存在諸多問題?,F在將狀態定位在每一個文件之上,這樣恢復上傳時,直接跳過即可
封裝組件
寫了一大堆,其實以上代碼你直接復制也無法使用,這里我將此封裝了一個組件。大家可以去github下載文件,里面有使用案例 ,若有用記得隨手給個star,謝謝!
偷個懶,具體封裝組件的代碼就不列出來了,大家直接去下載文件查看,若有不明白的,可留言。
組件文檔
Attribute
Slot
后端接口文檔:按文檔實現即可
代碼地址:https://github.com/pseudo-god/vue-simple-upload
接口文檔地址 https://docs.apipost.cn/view/0e19f16d4470ed6b#287746
作者:Pseudo
轉發鏈接:https://segmentfault.com/a/1190000023434864
總結
以上是生活随笔為你收集整理的dio 上传文件报错_Vue+Element UI实现断点续传、分片上传、秒传的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: react router 级联路由_前端
- 下一篇: element ui 前台模板_用 Vu