1. 程式人生 > 程式設計 >JavaScript 中如何實現大檔案並行下載

JavaScript 中如何實現大檔案並行下載

相信有些小夥伴已經瞭解大檔案上傳的解決方案,在上傳大檔案時,為了提高上傳的效率,我們一般會使用 Blob.slice 方法對大檔案按照指定的大小進行切割,然後在開啟多執行緒進行分塊上傳,等所有分塊都成功上傳後,再通知服務端進行分塊合併。

那麼對大檔案下載來說,我們能否採用類似的思想呢?在服務端支援 Range 請求首部的條件下,我們也是可以實現多執行緒分塊下載的功能,具體如下圖所示:

JavaScript 中如何實現大檔案並行下載

看完上圖相信你對大檔案下載的方案,已經有了一定的瞭解。接下來,我們先來介紹 HTTP 範圍請求。

一、HTTP 範圍請求

HTTP 協議範圍請求允許伺服器只發送 HTTP 訊息的一部分到客戶端。範圍請求在傳送大的媒體檔案,或者與檔案下載的斷點續傳功能搭配使用時非常有用。如果在響應中存在 Accept-Ranges 首部(並且它的值不為 “none”),那麼表示該伺服器支援範圍請求。

在一個 Range 首部中,可以一次性請求多個部分,伺服器會以 multipart 檔案的形式將其返回。如果伺服器返回的是範圍響應,需要使用 206 Partial Content 狀態碼。假如所請求的範圍不合法,那麼伺服器會返回 416 Range Not Satisfiable 狀態碼,表示客戶端錯誤。伺服器允許忽略 Range 首部,從而返回整個檔案,狀態碼用 200 。

1.1 Range 語法

Range:<unit>=<range-start>-
Range:<unit>=<range-start>-<range-end>
Range:<unit>=<range-start>-<range-end>,<range-start>-<range-end>
Range:<unit>=<range-start>-<range-end>,<range-start>-<range-end>,<range-start>-<range-end>
  • unit:範圍請求所採用的單位,通常是位元組(bytes)。
  • <range-start>:一個整數,表示在特定單位下,範圍的起始值。
  • <range-end>:一個整數,表示在特定單位下,範圍的結束值。這個值是可選的,如果不存在,表示此範圍一直延伸到文件結束。

瞭解完 Range 語法之後,我們來看一下實際的使用示例:

1.1.1 單一範

$curlhttp://i.imgur.com/z4d4kWk.jpg-i-H"Range:bytes=0-1023"

1.1.2 多重範圍

$curlhttp://www.example.com-i-H"Range:bytes=0-50,100-150"

好了,HTTP 範圍請求的相關知識就先介紹到這裡,下面我們步入正題開始介紹如何實現大檔案下載。

二、如何實現大檔案下載

為了讓大家能夠更好地理解後面的內容,我們先來看一下整體的流程圖:

JavaScript 中如何實現大檔案並行下載

瞭解完大檔案下載的流程之後,我們先來定義上述流程中涉及的一些輔助函式。

2.1 定義輔助函式

2.1.1 定義 getContentLength 函式

顧名思義 getContentLength 函式,用於獲取檔案的長度。在該函式中,我們通過傳送 HEAD 請求,然後從響應頭中讀取 Content-Length 的資訊,進而獲取當前 url 對應檔案的內容長度。

functiongetContentLength(url){
returnnewPromise((resolve,reject)=>{
letxhr=newXMLHttpRequest();
xhr.open("HEAD",url);
xhr.send();
xhr.onload=function(){
www.cppcns.comresolve(
~~xhr.getResponseHeader("Content-Length")
);
};
xhr.onerror=reject;
});
}

2.1.2 定義 asyncPool 函式
javascript 中如何實現併發控制? 這篇文章中,我們介紹了 asyncPool 函式,它用於實現非同步任務的併發控制。該函式接收 3 個引數:

  • poolLimit(數字型別):表示限制的併發數;
  • array(陣列型別):表示任務陣列;
  • iteratorFn(函式型別):表示迭代函式,用於實現對每個任務項進行處理,該函式會返回一個 Promise 物件或非同步函式。
asyncfunctionasyncPool(poolLimit,array,iteratorFn){
constret=[];//儲存所有的非同步任務
constexecuting=[];//儲存正在執行的非同步任務
for(constitemofarray){
//呼叫iteratorFn函式建立非同步任務
constp=Promise.resolve().then(()=>iteratorFn(item,array));
ret.push(p);//儲存新的非同步任務

//當poolLimit值小於或等於總任務個數時,進行併發控制
if(poolLimit<=array.length){
//當任務完成後,從正在執行的任務陣列中移除已完成的任務
conste=p.then(()=>executing.splice(executing.indexOf(e),1));
executing.push(e);//儲存正在執行的非同步任務
if(executing.length>=poolLimit){
awaitPromise.race(executing);//等待較快的任務執行完成
}
}
}
returnPromise.all(ret);
}

2.1.3 定義 getBinaryContent 函式
getBinaryContent 函式用於根據傳入的引數發起範圍請求,從而下載指定範圍內的檔案資料塊:

functiongetBinaryContent(url,start,end,i){
returnnewPromise((resolve,reject)=>{
try{
letxhr=newXMLHttpRequest();
xhr.open("GET",url,true);
xhr.setRequestHeader("range",`bytes=${start}-${end}`);//請求頭上設定範圍請求資訊
xhr.responseType="arraybuffer";//設定返回的型別為arraybuffer
xhr.onload=function(){
resolve({
index:i,//檔案塊的索引
buffer:xhr.response,//範圍請求對應的資料
});
};
xhr.send();
}catch(err){
reject(newError(err));
}
});
}

需要注意的是 ArrayBuffer 物件用來表示通用的、固定長度的原始二進位制資料緩衝區。我們不能直接操作 ArrayBuffer 的內容,而是要通過型別陣列物件或 DataView 物件來操作,它們會將緩衝區中的資料表示為特定的格式,並通過這些格式來讀寫緩衝區的內容。

2.1.4 定義 concatenate 函式
由於不能直接操作 ArrayBuffer 物件,所以我們需要先把 ArrayBuffer 物件轉換為 Uint8Array 物件,然後在執行合併操作。以下定義的 concatenate 函式就是為了合併已下載的檔案資料塊,具體程式碼如下所示:

functionconcatenate(arrays){
if(!arrays.length)returnnull;
lettotalLength=arrays.reduce((acc,value)=>acc+value.length,0);
letresult=newUint8Array(totalLength);
letlength=0;
for(letarrayofarrays){
result.set(array,length);
length+=array.length;
}
returnresult;
}

2.1.5 定義 saveAs 函式
saveAs 函式用於實現客戶端檔案儲存的功能,這裡只是一個簡單的實現。在實際專案中,你可以考慮直接使用 FileSaver.js

functionsaveAs({name,buffers,mime="application/octet-stream"}){
constblob=newBlob([buffers],{type:mime});
constblobUrl=URL.createObjectURL(blob);
http://www.cppcns.comconsta=document.createElement("a");
a.download=name||Math.random();
a.href=blobUrl;
a.click();
URL.revokeObjectURL(blob);
}

在 saveAs 函式中,我們使用了 Blob 和 Object URL。其中 Object URL 是一種偽協議,允許 Blob 和 File 物件用作影象,下載二進位制資料鏈接等的 URL 源。在瀏覽器中,我們使用 URL.createObjectURL 方法來建立 Object URL,該方法接收一個 Blob 物件,併為其建立一個唯一的 URL,其形式為 blob:<origin>/<uuid>,對應的示例如下:

blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

瀏覽器內部為每個通過 URL.createObjectURL 生成的 URL 儲存了一個 URL → Blob 對映。因此,此類 URL 較短,但可以訪問 Blob。生成的 URL 僅在當前文件開啟的狀態下才有效。

好了,Object URL 的相關內容就先介紹到這裡

2.1.6 定義 download 函式

download 函式用於實現下載操作,它支援 3 個引數:

  • url(字串型別):預下載資源的地址;
  • chunkSize(數字型別):分塊的大小,單位為位元組;
  • poolLimit(數字型別):表示限制的併發數。
asyncfunctiondownload({url,chunkSize,poolLimit=1}){
constcontenwww.cppcns.comtLength=awaitgetContentLength(url);
constchunks=typeofchunkSize==="number"?Math.ceil(contentLength/chunkSize):1;
constresults=awaitasyncPool(
&nb程式設計客棧sp;poolLimit,[...newArray(chunks).keys()],(i)=>{
letstart=i*chunkSize;
letend=i+1==chunks?contentLength-1:(i+1)*chunkSize-1;
returngetBinaryContent(url,i);
}
);
constsortedBuffers=results
.map((item)=>newUint8Array(item.buffer));
returnconcatenate(sortedBuffers);
}

2.2 大檔案下載使用示例

基於前面定義的輔助函式,我們就可以輕鬆地實現大檔案並行下載,具體程式碼如下所示:

functionmultiThreadedDownload(){
consturl=document.querySelector("#fileUrl").value;
if(!url||!/https?/.test(url))return;
console.log("多執行緒下載開始:"++newDate());
download({
url,chunkSize:0.1*1024*1024,poolLimit:6,}).then((buffers)=>{
console.log("多執行緒下載結束:"++newDate());
saveAs({buffers,name:"我的壓縮包"http://www.cppcns.com,mime:"application/zip"});
});
}

由於完整的示例程式碼內容比較多,阿寶哥就不放具體的程式碼了。感興趣的小夥伴,可以訪問以下地址瀏覽示例程式碼。

完整的示例程式碼:https://gist.github.com/semlinker/837211c039e6311e1e7629e5ee5f0a42

這裡我們來看一下大檔案下載示例的執行結果:

JavaScript 中如何實現大檔案並行下載

三、總結

本文介紹了在 javaScript 中如何利用 async-pool 這個庫提供的 asyncPool 函式,來實現大檔案的並行下載。除了介紹 asyncPool 函式之外,阿寶哥還介紹瞭如何通過 HEAD 請求獲取檔案大小、如何發起 HTTP 範圍請求及在客戶端如何儲存檔案等相關知識。其實利用 asyncPool 函式不僅可以實現大檔案的並行下載,而且還可以實現大檔案的並行上傳,感興趣的小夥伴可以自行嘗試一下。

以上就是JavaScript 中如何實現大檔案並行下載的詳細內容,更多關於JavaScript 大檔案並行下載的資料請關注我們其它相關文章!