一斋荞、是什么
不管怎樣簡單的需求,在量級(jí)達(dá)到一定層次時(shí)虐秦,都會(huì)變得異常復(fù)雜
文件上傳簡單平酿,文件變大就復(fù)雜
上傳大文件時(shí),以下幾個(gè)變量會(huì)影響我們的用戶體驗(yàn)
- 服務(wù)器處理數(shù)據(jù)的能力
- 請(qǐng)求超時(shí)
- 網(wǎng)絡(luò)波動(dòng)
上傳時(shí)間會(huì)變長悦陋,高頻次文件上傳失敗蜈彼,失敗后又需要重新上傳等等
為了解決上述問題,我們需要對(duì)大文件上傳單獨(dú)處理
這里涉及到分片上傳及斷點(diǎn)續(xù)傳兩個(gè)概念
分片上傳
分片上傳俺驶,就是將所要上傳的文件幸逆,按照一定的大小,將整個(gè)文件分隔成多個(gè)數(shù)據(jù)塊(Part)來進(jìn)行分片上傳
如下圖
上傳完之后再由服務(wù)端對(duì)所有上傳的文件進(jìn)行匯總整合成原始的文件
大致流程如下:
- 將需要上傳的文件按照一定的分割規(guī)則,分割成相同大小的數(shù)據(jù)塊还绘;
- 初始化一個(gè)分片上傳任務(wù)楚昭,返回本次分片上傳唯一標(biāo)識(shí);
- 按照一定的策略(串行或并行)發(fā)送各個(gè)分片數(shù)據(jù)塊拍顷;
- 發(fā)送完成后抚太,服務(wù)端根據(jù)判斷數(shù)據(jù)上傳是否完整,如果完整菇怀,則進(jìn)行數(shù)據(jù)塊合成得到原始文件
斷點(diǎn)續(xù)傳
斷點(diǎn)續(xù)傳指的是在下載或上傳時(shí)凭舶,將下載或上傳任務(wù)人為的劃分為幾個(gè)部分
每一個(gè)部分采用一個(gè)線程進(jìn)行上傳或下載,如果碰到網(wǎng)絡(luò)故障爱沟,可以從已經(jīng)上傳或下載的部分開始繼續(xù)上傳下載未完成的部分帅霜,而沒有必要從頭開始上傳下載。用戶可以節(jié)省時(shí)間呼伸,提高速度
一般實(shí)現(xiàn)方式有兩種:
- 服務(wù)器端返回身冀,告知從哪開始
- 瀏覽器端自行處理
上傳過程中將文件在服務(wù)器寫為臨時(shí)文件,等全部寫完了(文件上傳完)括享,將此臨時(shí)文件重命名為正式文件即可
如果中途上傳中斷過搂根,下次上傳的時(shí)候根據(jù)當(dāng)前臨時(shí)文件大小,作為在客戶端讀取文件的偏移量铃辖,從此位置繼續(xù)讀取文件數(shù)據(jù)塊剩愧,上傳到服務(wù)器從此偏移量繼續(xù)寫入文件即可
二、實(shí)現(xiàn)思路
整體思路比較簡單娇斩,拿到文件仁卷,保存文件唯一性標(biāo)識(shí),切割文件犬第,分段上傳锦积,每次上傳一段,根據(jù)唯一性標(biāo)識(shí)判斷文件上傳進(jìn)度歉嗓,直到文件的全部片段上傳完畢
下面的內(nèi)容都是偽代碼
讀取文件內(nèi)容:
const input = document.querySelector('input');
input.addEventListener('change', function() {
var file = this.files[0];
});
可以使用md5
實(shí)現(xiàn)文件的唯一性
const md5code = md5(file);
然后開始對(duì)文件進(jìn)行分割
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
//每10M切割一段,這里只做一個(gè)切割演示丰介,實(shí)際切割需要循環(huán)切割,
var slice = e.target.result.slice(0, 10*1024*1024);
});
h5上傳一個(gè)(一片)
const formdata = new FormData();
formdata.append('0', slice);
//這里是有一個(gè)坑的鉴分,部分設(shè)備無法獲取文件名稱哮幢,和文件類型,這個(gè)在最后給出解決方案
formdata.append('filename', file.filename);
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function() {
//xhr.responseText
});
xhr.open('POST', '');
xhr.send(formdata);
xhr.addEventListener('progress', updateProgress);
xhr.upload.addEventListener('progress', updateProgress);
function updateProgress(event) {
if (event.lengthComputable) {
//進(jìn)度條
}
}
這里給出常見的圖片和視頻的文件類型判斷
function checkFileType(type, file, back) {
/**
* type png jpg mp4 ...
* file input.change=> this.files[0]
* back callback(boolean)
*/
var args = arguments;
if (args.length != 3) {
back(0);
}
var type = args[0]; // type = '(png|jpg)' , 'png'
var file = args[1];
var back = typeof args[2] == 'function' ? args[2] : function() {};
if (file.type == '') {
// 如果系統(tǒng)無法獲取文件類型志珍,則讀取二進(jìn)制流家浇,對(duì)二進(jìn)制進(jìn)行解析文件類型
var imgType = [
'ff d8 ff', //jpg
'89 50 4e', //png
'0 0 0 14 66 74 79 70 69 73 6F 6D', //mp4
'0 0 0 18 66 74 79 70 33 67 70 35', //mp4
'0 0 0 0 66 74 79 70 33 67 70 35', //mp4
'0 0 0 0 66 74 79 70 4D 53 4E 56', //mp4
'0 0 0 0 66 74 79 70 69 73 6F 6D', //mp4
'0 0 0 18 66 74 79 70 6D 70 34 32', //m4v
'0 0 0 0 66 74 79 70 6D 70 34 32', //m4v
'0 0 0 14 66 74 79 70 71 74 20 20', //mov
'0 0 0 0 66 74 79 70 71 74 20 20', //mov
'0 0 0 0 6D 6F 6F 76', //mov
'4F 67 67 53 0 02', //ogg
'1A 45 DF A3', //ogg
'52 49 46 46 x x x x 41 56 49 20', //avi (RIFF fileSize fileType LIST)(52 49 46 46,DC 6C 57 09,41 56 49 20,4C 49 53 54)
];
var typeName = [
'jpg',
'png',
'mp4',
'mp4',
'mp4',
'mp4',
'mp4',
'm4v',
'm4v',
'mov',
'mov',
'mov',
'ogg',
'ogg',
'avi',
];
var sliceSize = /png|jpg|jpeg/.test(type) ? 3 : 12;
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
var slice = e.target.result.slice(0, sliceSize);
reader = null;
if (slice && slice.byteLength == sliceSize) {
var view = new Uint8Array(slice);
var arr = [];
view.forEach(function(v) {
arr.push(v.toString(16));
});
view = null;
var idx = arr.join(' ').indexOf(imgType);
if (idx > -1) {
back(typeName[idx]);
} else {
arr = arr.map(function(v) {
if (i > 3 && i < 8) {
return 'x';
}
return v;
});
var idx = arr.join(' ').indexOf(imgType);
if (idx > -1) {
back(typeName[idx]);
} else {
back(false);
}
}
} else {
back(false);
}
});
} else {
var type = file.name.match(/\.(\w+)$/)[1];
back(type);
}
}
調(diào)用方法如下
checkFileType('(mov|mp4|avi)',file,function(fileType){
// fileType = mp4,
// 如果file的類型不在枚舉之列,則返回false
});
上面上傳文件的一步碴裙,可以改成:
formdata.append('filename', md5code+'.'+fileType);
有了切割上傳后钢悲,也就有了文件唯一標(biāo)識(shí)信息点额,斷點(diǎn)續(xù)傳變成了后臺(tái)的一個(gè)小小的邏輯判斷
后端主要做的內(nèi)容為:根據(jù)前端傳給后臺(tái)的md5
值,到服務(wù)器磁盤查找是否有之前未完成的文件合并信息(也就是未完成的半成品文件切片)莺琳,取到之后根據(jù)上傳切片的數(shù)量还棱,返回?cái)?shù)據(jù)告訴前端開始從第幾節(jié)上傳
如果想要暫停切片的上傳,可以使用XMLHttpRequest
的 abort
方法
三惭等、使用場景
- 大文件加速上傳:當(dāng)文件大小超過預(yù)期大小時(shí)珍手,使用分片上傳可實(shí)現(xiàn)并行上傳多個(gè) Part, 以加快上傳速度
- 網(wǎng)絡(luò)環(huán)境較差:建議使用分片上傳辞做。當(dāng)出現(xiàn)上傳失敗的時(shí)候琳要,僅需重傳失敗的Part
- 流式上傳:可以在需要上傳的文件大小還不確定的情況下開始上傳。這種場景在視頻監(jiān)控等行業(yè)應(yīng)用中比較常見
小結(jié)
當(dāng)前的偽代碼秤茅,只是提供一個(gè)簡單的思路稚补,想要把事情做到極致,我們還需要考慮到更多場景框喳,比如
- 切片上傳失敗怎么辦
- 上傳過程中刷新頁面怎么辦
- 如何進(jìn)行并行上傳
- 切片什么時(shí)候按數(shù)量切课幕,什么時(shí)候按大小切
- 如何結(jié)合 Web Work 處理大文件上傳
- 如何實(shí)現(xiàn)秒傳
人生又何嘗不是如此,極致的人生體驗(yàn)有無限可能五垮,越是后面才發(fā)現(xiàn)越是精彩 _