js自己實(shí)現(xiàn)一個(gè)大文件切片上傳+斷點(diǎn)續(xù)傳的示例代碼
PM:喂,那個(gè)切圖仔,我這里有個(gè)100G的視頻要上傳,你幫我做一個(gè)上傳后臺(tái),下班前給我哦,辛苦了。
我:。。。
相信每個(gè)切圖工程師,都接觸過(guò)文件上傳的需求,一般的小文件,我們直接使用 input file,然后構(gòu)造一個(gè) new FormData()對(duì)象,扔給后端就可以了。如果使用了 Ant design 或者 element ui 之類(lèi)的ui庫(kù),那更簡(jiǎn)單,直接調(diào)用一下api即可。當(dāng)然了,復(fù)雜一些的,市面上也有不少優(yōu)秀的第三方插件,比如WebUploader。但是作為一個(gè)有追求的工程師,怎么能僅僅滿(mǎn)足于使用插件呢,今天我們就來(lái)自己實(shí)現(xiàn)一個(gè)。
首先我們來(lái)分析一下需求
一個(gè)上傳組件,需要具備的功能:
- 需要校驗(yàn)文件格式
- 可以上傳任何文件,包括超大的視頻文件(切片)
- 上傳期間斷網(wǎng)后,再次聯(lián)網(wǎng)可以繼續(xù)上傳(斷點(diǎn)續(xù)傳)
- 要有進(jìn)度條提示
- 已經(jīng)上傳過(guò)同一個(gè)文件后,直接上傳完成(秒傳)
前后端分工:
前端:
- 文件格式校驗(yàn)
- 文件切片、md5計(jì)算
- 發(fā)起檢查請(qǐng)求,把當(dāng)前文件的hash發(fā)送給服務(wù)端,檢查是否有相同hash的文件
- 上傳進(jìn)度計(jì)算
- 上傳完成后通知后端合并切片
后端:
- 檢查接收到的hash是否有相同的文件,并通知前端當(dāng)前hash是否有未完成的上傳
- 接收切片
- 合并所有切片
架構(gòu)圖如下

接下來(lái)開(kāi)始具體實(shí)現(xiàn)
一、 格式校驗(yàn)
對(duì)于上傳的文件,一般來(lái)說(shuō),我們要校驗(yàn)其格式,僅需要獲取文件的后綴(擴(kuò)展名),即可判斷其是否符合我們的上傳限制:
//文件路徑
var filePath = "file://upload/test.png";
//獲取最后一個(gè).的位置
var index= filePath.lastIndexOf(".");
//獲取后綴
var ext = filePath.substr(index+1);
//輸出結(jié)果
console.log(ext);
// 輸出: png但是,這種方式有個(gè)弊端,那就是我們可以隨便篡改文件的后綴名,比如:test.mp4 ,我們可以通過(guò)修改其后綴名:test.mp4 -> test.png ,這樣即可繞過(guò)限制進(jìn)行上傳。那有沒(méi)有更嚴(yán)格的限制方式呢?當(dāng)然是有的。
那就是通過(guò)查看文件的二進(jìn)制數(shù)據(jù)來(lái)識(shí)別其真實(shí)的文件類(lèi)型,因?yàn)橛?jì)算機(jī)識(shí)別文件類(lèi)型時(shí),并不是真的通過(guò)文件的后綴名來(lái)識(shí)別的,而是通過(guò) “魔數(shù)”(Magic Number)來(lái)區(qū)分,對(duì)于某一些類(lèi)型的文件,起始的幾個(gè)字節(jié)內(nèi)容都是固定的,根據(jù)這幾個(gè)字節(jié)的內(nèi)容就可以判斷文件的類(lèi)型。借助十六進(jìn)制編輯器,可以查看一下圖片的二進(jìn)制數(shù)據(jù),我們還是以test.png為例:

由上圖可知,PNG 類(lèi)型的圖片前 8 個(gè)字節(jié)是 0x89 50 4E 47 0D 0A 1A 0A?;谶@個(gè)結(jié)果,我們可以據(jù)此來(lái)做文件的格式校驗(yàn),以vue項(xiàng)目為例:
??<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個(gè)字節(jié),即可識(shí)別其類(lèi)型
??????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后,打印結(jié)果為true
??????console.log(isPNG(uint8Array))
????},
????readBuffer(file,?start?=?0,?end?=?2)?{
??????//?獲取文件的二進(jìn)制數(shù)據(jù),因?yàn)槲覀冎恍枰r?yàn)前幾個(gè)字節(jié)即可,所以并不需要獲取整個(gè)文件的數(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>以上為校驗(yàn)文件類(lèi)型的方法,對(duì)于其他類(lèi)型的文件,比如mp4,xsl等,大家感興趣的話(huà),也可以通過(guò)工具查看其二進(jìn)制數(shù)據(jù),以此來(lái)做格式校驗(yàn)。
以下為匯總的一些文件的二進(jìn)制標(biāo)識(shí):
1.JPEG/JPG - 文件頭標(biāo)識(shí) (2 bytes): ff, d8 文件結(jié)束標(biāo)識(shí) (2 bytes): ff, d9
2.TGA - 未壓縮的前 5 字節(jié) 00 00 02 00 00 - RLE 壓縮的前 5 字節(jié) 00 00 10 00 00
3.PNG - 文件頭標(biāo)識(shí) (8 bytes) 89 50 4E 47 0D 0A 1A 0A
4.GIF - 文件頭標(biāo)識(shí) (6 bytes) 47 49 46 38 39(37) 61
5.BMP - 文件頭標(biāo)識(shí) (2 bytes) 42 4D B M
6.PCX - 文件頭標(biāo)識(shí) (1 bytes) 0A
7.TIFF - 文件頭標(biāo)識(shí) (2 bytes) 4D 4D 或 49 49
8.ICO - 文件頭標(biāo)識(shí) (8 bytes) 00 00 01 00 01 00 20 20
9.CUR - 文件頭標(biāo)識(shí) (8 bytes) 00 00 02 00 01 00 20 20
10.IFF - 文件頭標(biāo)識(shí) (4 bytes) 46 4F 52 4D
11.ANI - 文件頭標(biāo)識(shí) (4 bytes) 52 49 46 46
二、 文件切片
假設(shè)我們要把一個(gè)1G的視頻,分割為每塊1MB的切片,可定義 DefualtChunkSize = 1 * 1024 * 1024,通過(guò) spark-md5來(lái)計(jì)算文件內(nèi)容的hash值。那如何分割文件呢,使用文件對(duì)象File的方法File.prototype.slice即可。
需要注意的是,切割一個(gè)較大的文件,比如10G,那分割為1Mb大小的話(huà),將會(huì)生成一萬(wàn)個(gè)切片,眾所周知,js是單線(xiàn)程模型,如果這個(gè)計(jì)算過(guò)程在主線(xiàn)程中的話(huà),那我們的頁(yè)面必然會(huì)直接崩潰,這時(shí),就該我們的 Web Worker 來(lái)上場(chǎng)了。
Web Worker 的作用,就是為 JavaScript 創(chuàng)造多線(xiàn)程環(huán)境,允許主線(xiàn)程創(chuàng)建 Worker 線(xiàn)程,將一些任務(wù)分配給后者運(yùn)行。在主線(xiàn)程運(yùn)行的同時(shí),Worker 線(xiàn)程在后臺(tái)運(yùn)行,兩者互不干擾。具體的作用,不了解的同學(xué)可以自行去學(xué)些一下。這里就不展開(kāi)講了。
以下為部分關(guān)鍵代碼:
??//?upload.js
??//?創(chuàng)建一個(gè)worker對(duì)象
??const?worker?=?new?worker('worker.js')
??//?向子線(xiàn)程發(fā)送消息,并傳入文件對(duì)象和切片大小,開(kāi)始計(jì)算分割切片
??worker.postMessage(file,?DefualtChunkSize)
??//?子線(xiàn)程計(jì)算完成后,會(huì)將切片返回主線(xiàn)程
??worker.onmessage?=?(chunks)?=>?{
????...
??}子線(xiàn)程代碼:
??//?worker.js
??//?接收文件對(duì)象及切片大小
??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);
????????//?此處為重點(diǎn),計(jì)算完成后,仍然通過(guò)postMessage通知主線(xiàn)程
????????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線(xiàn)程,我們即可得到計(jì)算后的切片,以及md5值。
三、 斷點(diǎn)續(xù)傳 + 秒傳 + 上傳進(jìn)度
在拿到切片和md5后,我們首先去服務(wù)器查詢(xún)一下,是否已經(jīng)存在當(dāng)前文件。
- 如果已存在,并且已經(jīng)是上傳成功的文件,則直接返回前端上傳成功,即可實(shí)現(xiàn)"秒傳"。
- 如果已存在,并且有一部分切片上傳失敗,則返回給前端已經(jīng)上傳成功的切片name,前端拿到后,根據(jù)返回的切片,計(jì)算出未上傳成功的剩余切片,然后把剩余的切片繼續(xù)上傳,即可實(shí)現(xiàn)"斷點(diǎn)續(xù)傳"。
- 如果不存在,則開(kāi)始上傳,這里需要注意的是,在并發(fā)上傳切片時(shí),需要控制并發(fā)量,避免一次性上傳過(guò)多切片,導(dǎo)致崩潰。
//?檢查是否已存在相同文件
???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)?//當(dāng)前是第幾片?????
????form.append("fileMd5Value",?fileMd5Value)
????return?axios({
??????method:?'post',
??????url:?BaseUrl?+?"/upload",
??????data:?form
????}).then(({?data?})?=>?{
??????if?(data.stat)?{
????????current?=?current?+?1
????????//?獲取到上傳的進(jìn)度
????????const?uploadPercent?=?Math.ceil((current?/?chunkList.length)?*?100)
??????}
????})
??}所有切片上傳完成后,再向后端發(fā)送一個(gè)上傳完成的請(qǐng)求,即通知后端把所有切片進(jìn)行合并,最終完成整個(gè)上傳流程。
大功告成!由于篇幅有限,本文主要講了前端的實(shí)現(xiàn)思路,最終落地成完整的項(xiàng)目,還是需要大家根據(jù)真實(shí)的項(xiàng)目需求來(lái)實(shí)現(xiàn)。
到此這篇關(guān)于js自己實(shí)現(xiàn)一個(gè)大文件切片上傳+斷點(diǎn)續(xù)傳的示例代碼的文章就介紹到這了,更多相關(guān)js大文件切片上傳+斷點(diǎn)續(xù)傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS+HTML5實(shí)現(xiàn)上傳圖片預(yù)覽效果完整實(shí)例【測(cè)試可用】
這篇文章主要介紹了JS+HTML5實(shí)現(xiàn)上傳圖片預(yù)覽效果,結(jié)合完整實(shí)例形式分析了javascript上傳圖片本地預(yù)覽的具體操作步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-04-04
JavaScript DSL 流暢接口(使用鏈?zhǔn)秸{(diào)用)實(shí)例
這篇文章主要介紹了JavaScript DSL 流暢接口(使用鏈?zhǔn)秸{(diào)用)實(shí)例,本文講解了DSL 流暢接口、DSL 表達(dá)式生成器等內(nèi)容,需要的朋友可以參考下2015-03-03
js代碼運(yùn)行報(bào)錯(cuò)Warning:To?load?an?ES?module,?set?"type&q
最近在學(xué)習(xí)ES6的過(guò)程中,和運(yùn)行javascript文件時(shí)進(jìn)行了報(bào)錯(cuò),下面這篇文章主要給大家介紹了關(guān)于js代碼運(yùn)行報(bào)錯(cuò)Warning:To?load?an?ES?module,?set?"type":"module"in?the?package.json?or?use?the?.mjs的相關(guān)資料,需要的朋友可以參考下2023-04-04
JavaScript 上萬(wàn)關(guān)鍵字瞬間匹配實(shí)現(xiàn)代碼
發(fā)一篇之前寫(xiě)的文章,平時(shí)還是經(jīng)常用到的,尤其是河蟹詞特別多的聊天系統(tǒng)里2013-07-07
javascript常見(jiàn)數(shù)字進(jìn)制轉(zhuǎn)換實(shí)例分析
這篇文章主要介紹了javascript常見(jiàn)數(shù)字進(jìn)制轉(zhuǎn)換,結(jié)合實(shí)例形式分析了JavaScript十進(jìn)制,十六進(jìn)制及二進(jìn)制的相互轉(zhuǎn)換原理與技巧,需要的朋友可以參考下2016-04-04

