大文件上傳
大文件使用一次請求發(fā)送的話,那么這個請求的時間就會非常的長。一旦請求的過程中出現(xiàn)一些問題,比如網(wǎng)絡(luò)斷開了,那么你就不得不把整個文件重新上傳一遍。這樣的代價是用戶接受不了的,也非常的浪費(fèi)資源。所以我們在做大文件上傳的時候,我們往往會對文件進(jìn)行分片。
分片原理
在客戶端,首先把整個的大文件數(shù)據(jù),分成一個一個的數(shù)據(jù)小塊,你可以把每一塊想象成為單獨的一個小文件,然后利用單文件上傳把這些小文件依次傳到服務(wù)器,當(dāng)最后把文件全部傳輸完成之后在服務(wù)器端使用程序把整個文件的小數(shù)據(jù)組裝起來,形成一個完成的文件。這個組裝由后端完成!
在這個過程中前端要做到最核心的就是要把文件進(jìn)行分片,分成一個一個的小塊。所以大文件的上傳的第一個核心技術(shù)點就是文件如何來分片。
舉例分析
// splitFile.html
<html>
<head> </head>
<body>
<input type="file"/>
<script>
const inp = document.querySelector('input');
inp.onchange = (e) => {
const file = inp.files[0];
if(!file) return;
console.log(file);
}
</script>
</body>
</html>
在瀏覽器中打開這個例子,這里有一個input元素,然后監(jiān)聽它的onchange事件,拿到它選擇的文件。選擇一個文件,我們得到的是一個File對象

接下來我們對這個File對象進(jìn)行切片,這個對象里面有一個函數(shù)叫做slice,它跟數(shù)組的slice一個用法,它的起始和結(jié)束就表示說從這個文件第多少個字節(jié)開始取,取到第多少個字節(jié)。file.slice(0,100)表示取該文件的0-99個字節(jié),得到一個文件的切片數(shù)據(jù)。修改onchange方法如下:
const file = inp.files[0];
if(!file) return;
// 0-99個字節(jié)
const piece = file.slice(0,100);
console.log(piece);
打印結(jié)果我們發(fā)現(xiàn)分片后的數(shù)據(jù)是一個Blob類型,我們知道Blob類型它也是表示文件數(shù)據(jù)的,也就是說用ajax請求的時候可以直接把它發(fā)送到服務(wù)器。它的用法和File對象的用法是一樣的。
有了slice函數(shù)我們就可以非常輕松的完成切片,我們寫一個工具函數(shù),并在拿到File對象后使用該函數(shù)進(jìn)行分割如下:
<html>
<head></head>
<body>
<input type="file"/>
<script>
const inp = document.querySelector('input');
inp.onchange = (e) => {
const file = inp.files[0];
if(!file) return;
//這里一次分割50k,獲取到分片結(jié)果
const chunks = createChunks(file, 50 *1024);
console.log(chunks);
}
/*
file:File對象,
chunkSize: 切片的大小
*/
function createChunks(file, chunkSize) {
const result = [];
for (let i = 0; i < file.size; i+=chunkSize) {
result.push(file.slice(i, i + chunkSize));
}
return result;
}
</script>
</body>
</html>
打印一下,我們已經(jīng)獲取到了分片的結(jié)果是一個Blob數(shù)組。分片是很快的,原因是File或Blob對象其實只是保存了文件的基本信息,比如說File對象文件有多大、文件是什么類型、文件的名字和位置等這些基本信息。它并沒有保存文件的數(shù)據(jù)。Blob也是一樣的,它保存了數(shù)據(jù)的大小和類型。所以分片其實就是一個簡單的數(shù)學(xué)運(yùn)算。
到這里分片就結(jié)束了,我們真正讀取數(shù)據(jù)的時候需要利用FileReader才能真正的把它們的數(shù)據(jù)讀出來。
斷點續(xù)傳
有這樣一個場景,我們在分片上傳的時候網(wǎng)絡(luò)不好或斷網(wǎng)了,我們下一次要接著上傳之前上傳過的分片,之前上傳過的分片就不用再上傳了。這種文件秒傳是怎么操作的呢?看一下它的基本原理:

它其實就是跟服務(wù)器的一次對話,重新上傳同一個文件時會先訪問一下服務(wù)器,告訴服務(wù)器我要給你上傳一個文件,你告訴我一下這個文件我上傳過了沒有,還有哪些分片還沒有上傳。服務(wù)器會返回對應(yīng)的結(jié)果,告訴客戶端之前已經(jīng)上傳過了哪些分片了,還有哪些編號的分片還需要傳遞。
通過一次Ajax請求,客戶端就能夠知道這個文件我該如何處理,那么在這個交互過程中客戶端必須要告訴服務(wù)器一個關(guān)鍵的信息,一個能夠唯一代表這個文件的東西就是文件的hash值。hash是一種算法,它可以把任何數(shù)據(jù)換算成一個固定長度的字符串,這個字符串是不可逆的。
所以在上傳文件的時候需要生成一個hash值,并在服務(wù)器記錄一下,在下一次重傳的時候我再告訴你這個hash值之前有沒有傳過,還有哪些分片需要上傳,通過這個hash值就能夠代表整個的文件內(nèi)容。這里我們使用md5作為它的hash算法,接下來的關(guān)鍵點就是我們?nèi)绾卧诳蛻舳擞嬎愠鲞@個文件的hash值,我們可以使用一個第三方庫spark-md5來做這個事。
<html>
<head> </head>
<body>
<input type="file"/>
<script src="./spark-md5.js"></script>
<script>
const inp = document.querySelector('input');
inp.onchange = (e) => {
const file = inp.files[0];
if(!file) return;
//這里一次分割50k,獲取到分片結(jié)果
const chunks = createChunks(file, 50 *1024);
hash(chunks);
}
/*
生成文件hsah值,
為了防止文件過大,采用分塊增量算法
*/
function hash(chunks) {
const spark = new SparkMD5();
// 遞歸函數(shù)
function _read(i) {
if(i >= chunks.length) {
console.log(spark.end());
return; //讀取完成
}
const blob = chunks[i];
const reader = new FileReader();
reader.onload = e => {
// 讀取到的字節(jié)數(shù)組
const bytes = e.target.result;
spark.append(bytes);
_read(i+1);
}
reader.readAsArrayBuffer(blob);
}
_read(0);
}
/*
file:File對象,
chunkSize: 切片的大小
*/
function createChunks(file, chunkSize) {
const result = [];
for (let i = 0; i < file.size; i+=chunkSize) {
result.push(file.slice(i, i + chunkSize));
}
return result;
}
</script>
</body>
</html>
上面代碼中,為了防止文件過大,我們采用分塊增量算法來獲取hash值。修改代碼選擇文件,我們就可以打印這個文件的hash值了。如果文件過大計算量也將變大,就沒切片時那么的快了。
接下來我們把hash函數(shù)封裝成一個異步函數(shù),讓它返回一個Promise:
<html>
<head></head>
<body>
<input type="file" />
<script src="./spark-md5.js"></script>
<script>
const inp = document.querySelector('input');
inp.onchange = async (e) => {
const file = inp.files[0];
if (!file) return;
//這里一次分割50k,獲取到分片結(jié)果
const chunks = createChunks(file, 50 * 1024);
const result = await hash(chunks);
console.log(result);
}
/*
生成文件hsah值,
為了防止文件過大,采用分塊增量算法
*/
function hash(chunks) {
return new Promise((resolve) => {
const spark = new SparkMD5();
// 遞歸函數(shù)
function _read(i) {
if (i >= chunks.length) {
resolve(spark.end());
return; //讀取完成
}
const blob = chunks[i];
const reader = new FileReader();
reader.onload = e => {
// 讀取到的字節(jié)數(shù)組
const bytes = e.target.result;
spark.append(bytes);
_read(i + 1);
}
reader.readAsArrayBuffer(blob);
}
_read(0);
})
}
/*
file:File對象,
chunkSize: 切片的大小
*/
function createChunks(file, chunkSize) {
const result = [];
for (let i = 0; i < file.size; i += chunkSize) {
// slice越界則取數(shù)據(jù)最大長度
result.push(file.slice(i, i + chunkSize));
}
return result;
}
</script>
</body>
</html>
為了防止主線程卡死,我們一般不會放到主線程里的,我們可以利用web worker單獨去開一個線程。因為這個操作時CPU密集型任務(wù),如果放到單獨線程還是卡頓,可能是特別大的文件造成的,這個時候可以先粗略的對這個文件分成一些大塊,單獨去計算每個大塊,比如每個大塊有300M的數(shù)據(jù)量,將這個大塊再分小塊,這樣計算它的hash是非常快的。當(dāng)有空閑的時候再去慢慢計算后邊的hash值,因為后邊的hash值還不著急用。