js自己實現(xiàn)一個大文件切片上傳+斷點續(xù)傳的示例代碼
PM:喂,那個切圖仔,我這里有個100G的視頻要上傳,你幫我做一個上傳后臺,下班前給我哦,辛苦了。
我:。。。
相信每個切圖工程師,都接觸過文件上傳的需求,一般的小文件,我們直接使用 input file,然后構造一個 new FormData()對象,扔給后端就可以了。如果使用了 Ant design 或者 element ui 之類的ui庫,那更簡單,直接調(diào)用一下api即可。當然了,復雜一些的,市面上也有不少優(yōu)秀的第三方插件,比如WebUploader。但是作為一個有追求的工程師,怎么能僅僅滿足于使用插件呢,今天我們就來自己實現(xiàn)一個。
首先我們來分析一下需求
一個上傳組件,需要具備的功能:
- 需要校驗文件格式
- 可以上傳任何文件,包括超大的視頻文件(切片)
- 上傳期間斷網(wǎng)后,再次聯(lián)網(wǎng)可以繼續(xù)上傳(斷點續(xù)傳)
- 要有進度條提示
- 已經(jīng)上傳過同一個文件后,直接上傳完成(秒傳)
前后端分工:
前端:
- 文件格式校驗
- 文件切片、md5計算
- 發(fā)起檢查請求,把當前文件的hash發(fā)送給服務端,檢查是否有相同hash的文件
- 上傳進度計算
- 上傳完成后通知后端合并切片
后端:
- 檢查接收到的hash是否有相同的文件,并通知前端當前hash是否有未完成的上傳
- 接收切片
- 合并所有切片
架構圖如下
接下來開始具體實現(xiàn)
一、 格式校驗
對于上傳的文件,一般來說,我們要校驗其格式,僅需要獲取文件的后綴(擴展名),即可判斷其是否符合我們的上傳限制:
//文件路徑 var filePath = "file://upload/test.png"; //獲取最后一個.的位置 var index= filePath.lastIndexOf("."); //獲取后綴 var ext = filePath.substr(index+1); //輸出結果 console.log(ext); // 輸出: png
但是,這種方式有個弊端,那就是我們可以隨便篡改文件的后綴名,比如:test.mp4 ,我們可以通過修改其后綴名:test.mp4 -> test.png ,這樣即可繞過限制進行上傳。那有沒有更嚴格的限制方式呢?當然是有的。
那就是通過查看文件的二進制數(shù)據(jù)來識別其真實的文件類型,因為計算機識別文件類型時,并不是真的通過文件的后綴名來識別的,而是通過 “魔數(shù)”(Magic Number)來區(qū)分,對于某一些類型的文件,起始的幾個字節(jié)內(nèi)容都是固定的,根據(jù)這幾個字節(jié)的內(nèi)容就可以判斷文件的類型。借助十六進制編輯器,可以查看一下圖片的二進制數(shù)據(jù),我們還是以test.png為例:
由上圖可知,PNG 類型的圖片前 8 個字節(jié)是 0x89 50 4E 47 0D 0A 1A 0A?;谶@個結果,我們可以據(jù)此來做文件的格式校驗,以vue項目為例:
??<template> ??<div> ????<input ??????type="file" ??????id="inputFile" ??????@change="handleChange" ????/> ??</div> </template> <script> export?default?{ ??name:?"HelloWorld", ??methods:?{ ????check(headers)?{ ??????return?(buffers,?options?=?{?offset:?0?})?=> ??????headers.every( ??????(header,?index)?=>?header?===?buffers[options.offset?+?index] ??????); ????}, ????async?handleChange(event)?{ ??????const?file?=?event.target.files[0]; ??????//?以PNG為例,只需要獲取前8個字節(jié),即可識別其類型 ??????const?buffers?=?await?this.readBuffer(file,?0,?8); ??????const?uint8Array?=?new?Uint8Array(buffers); ??????const?isPNG?=?this.check([0x89,?0x50,?0x4e,?0x47,?0x0d,?0x0a,?0x1a,?0x0a]); ??????//?上傳test.png后,打印結果為true ??????console.log(isPNG(uint8Array)) ????}, ????readBuffer(file,?start?=?0,?end?=?2)?{ ??????//?獲取文件的二進制數(shù)據(jù),因為我們只需要校驗前幾個字節(jié)即可,所以并不需要獲取整個文件的數(shù)據(jù) ????????return?new?Promise((resolve,?reject)?=>?{ ??????????const?reader?=?new?FileReader(); ??????????reader.onload?=?()?=>?{ ????????????resolve(reader.result); ??????????}; ??????????reader.onerror?=?reject; ??????????reader.readAsArrayBuffer(file.slice(start,?end)); ????????}); ????} ??} }; </script>
以上為校驗文件類型的方法,對于其他類型的文件,比如mp4,xsl等,大家感興趣的話,也可以通過工具查看其二進制數(shù)據(jù),以此來做格式校驗。
以下為匯總的一些文件的二進制標識:
1.JPEG/JPG - 文件頭標識 (2 bytes): ff, d8 文件結束標識 (2 bytes): ff, d9
2.TGA - 未壓縮的前 5 字節(jié) 00 00 02 00 00 - RLE 壓縮的前 5 字節(jié) 00 00 10 00 00
3.PNG - 文件頭標識 (8 bytes) 89 50 4E 47 0D 0A 1A 0A
4.GIF - 文件頭標識 (6 bytes) 47 49 46 38 39(37) 61
5.BMP - 文件頭標識 (2 bytes) 42 4D B M
6.PCX - 文件頭標識 (1 bytes) 0A
7.TIFF - 文件頭標識 (2 bytes) 4D 4D 或 49 49
8.ICO - 文件頭標識 (8 bytes) 00 00 01 00 01 00 20 20
9.CUR - 文件頭標識 (8 bytes) 00 00 02 00 01 00 20 20
10.IFF - 文件頭標識 (4 bytes) 46 4F 52 4D
11.ANI - 文件頭標識 (4 bytes) 52 49 46 46
二、 文件切片
假設我們要把一個1G的視頻,分割為每塊1MB的切片,可定義 DefualtChunkSize = 1 * 1024 * 1024,通過 spark-md5來計算文件內(nèi)容的hash值。那如何分割文件呢,使用文件對象File的方法File.prototype.slice即可。
需要注意的是,切割一個較大的文件,比如10G,那分割為1Mb大小的話,將會生成一萬個切片,眾所周知,js是單線程模型,如果這個計算過程在主線程中的話,那我們的頁面必然會直接崩潰,這時,就該我們的 Web Worker 來上場了。
Web Worker 的作用,就是為 JavaScript 創(chuàng)造多線程環(huán)境,允許主線程創(chuàng)建 Worker 線程,將一些任務分配給后者運行。在主線程運行的同時,Worker 線程在后臺運行,兩者互不干擾。具體的作用,不了解的同學可以自行去學些一下。這里就不展開講了。
以下為部分關鍵代碼:
??//?upload.js ??//?創(chuàng)建一個worker對象 ??const?worker?=?new?worker('worker.js') ??//?向子線程發(fā)送消息,并傳入文件對象和切片大小,開始計算分割切片 ??worker.postMessage(file,?DefualtChunkSize) ??//?子線程計算完成后,會將切片返回主線程 ??worker.onmessage?=?(chunks)?=>?{ ????... ??}
子線程代碼:
??//?worker.js ??//?接收文件對象及切片大小 ??onmessage?(file,?DefualtChunkSize)?=>?{ ????let?blobSlice?=?File.prototype.slice?||?File.prototype.mozSlice?||?File.prototype.webkitSlice, ??????chunks?=?Math.ceil(file.size?/?DefualtChunkSize), ??????currentChunk?=?0, ??????spark?=?new?SparkMD5.ArrayBuffer(), ??????fileReader?=?new?FileReader(); ????fileReader.onload?=?function?(e)?{ ??????console.log('read?chunk?nr',?currentChunk?+?1,?'of'); ??????const?chunk?=?e.target.result; ??????spark.append(chunk); ??????currentChunk++; ??????if?(currentChunk?<?chunks)?{ ????????loadNext(); ??????}?else?{ ????????let?fileHash?=?spark.end(); ????????console.info('finished?computed?hash',?fileHash); ????????//?此處為重點,計算完成后,仍然通過postMessage通知主線程 ????????postMessage({?fileHash,?fileReader?}) ??????} ????}; ????fileReader.onerror?=?function?()?{ ??????console.warn('oops,?something?went?wrong.'); ????}; ????function?loadNext()?{ ??????let?start?=?currentChunk?*?DefualtChunkSize, ????????end?=?((start?+?DefualtChunkSize)?>=?file.size)???file.size?:?start?+?DefualtChunkSize; ??????let?chunk?=?blobSlice.call(file,?start,?end); ??????fileReader.readAsArrayBuffer(chunk); ????} ????loadNext(); ??}
以上利用worker線程,我們即可得到計算后的切片,以及md5值。
三、 斷點續(xù)傳 + 秒傳 + 上傳進度
在拿到切片和md5后,我們首先去服務器查詢一下,是否已經(jīng)存在當前文件。
- 如果已存在,并且已經(jīng)是上傳成功的文件,則直接返回前端上傳成功,即可實現(xiàn)"秒傳"。
- 如果已存在,并且有一部分切片上傳失敗,則返回給前端已經(jīng)上傳成功的切片name,前端拿到后,根據(jù)返回的切片,計算出未上傳成功的剩余切片,然后把剩余的切片繼續(xù)上傳,即可實現(xiàn)"斷點續(xù)傳"。
- 如果不存在,則開始上傳,這里需要注意的是,在并發(fā)上傳切片時,需要控制并發(fā)量,避免一次性上傳過多切片,導致崩潰。
//?檢查是否已存在相同文件 ???async?function?checkAndUploadChunk(chunkList,?fileMd5Value)?{ ????const?requestList?=?[] ????//?如果不存在,則上傳 ????for?(let?i?=?0;?i?<?chunkList;?i++)?{ ??????requestList.push(upload({?chunkList[i],?fileMd5Value,?i?})) ????} ????//?并發(fā)上傳 ????if?(requestList?.length)?{ ??????await?Promise.all(requestList) ????} ??} ?//?上傳chunk ??function?upload({?chunkList,?chunk,?fileMd5Value,?i?})?{ ????current?=?0 ????let?form?=?new?FormData() ????form.append("data",?chunk)?//切片流 ????form.append("total",?chunkList.length)?//總片數(shù) ????form.append("index",?i)?//當前是第幾片????? ????form.append("fileMd5Value",?fileMd5Value) ????return?axios({ ??????method:?'post', ??????url:?BaseUrl?+?"/upload", ??????data:?form ????}).then(({?data?})?=>?{ ??????if?(data.stat)?{ ????????current?=?current?+?1 ????????//?獲取到上傳的進度 ????????const?uploadPercent?=?Math.ceil((current?/?chunkList.length)?*?100) ??????} ????}) ??}
所有切片上傳完成后,再向后端發(fā)送一個上傳完成的請求,即通知后端把所有切片進行合并,最終完成整個上傳流程。
大功告成!由于篇幅有限,本文主要講了前端的實現(xiàn)思路,最終落地成完整的項目,還是需要大家根據(jù)真實的項目需求來實現(xiàn)。
到此這篇關于js自己實現(xiàn)一個大文件切片上傳+斷點續(xù)傳的示例代碼的文章就介紹到這了,更多相關js大文件切片上傳+斷點續(xù)傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
javascript不同類型數(shù)據(jù)之間的運算的轉換方法
這篇文章主要介紹了javascript不同類型數(shù)據(jù)之間的運算的轉換方法,需要的朋友可以參考下2014-02-02微信小程序使用webview打開pdf文檔以及顯示網(wǎng)頁內(nèi)容的方法步驟
在線查看PDF文件,已經(jīng)是很常見的需求了,下面這篇文章主要給大家介紹了關于微信小程序使用webview打開pdf文檔以及顯示網(wǎng)頁內(nèi)容的方法步驟,文中通過圖文介紹的非常詳細,需要的朋友可以參考下2022-07-07利用BootStrap的Carousel.js實現(xiàn)輪播圖動畫效果
這篇文章主要介紹了利用BootStrap的Carousel.js實現(xiàn)輪播圖動畫效果的相關資料,需要的朋友可以參考下2016-12-12javascript之querySelector和querySelectorAll使用介紹
其實關于querySelector和querySelectorAll的介紹說明很多。在此主要是做個記錄2011-12-12