JavaScript 中如何實現大檔案並行下載
相信有些小夥伴已經瞭解大檔案上傳的解決方案,在上傳大檔案時,為了提高上傳的效率,我們一般會使用 Blob.slice 方法對大檔案按照指定的大小進行切割,然後在開啟多執行緒進行分塊上傳,等所有分塊都成功上傳後,再通知服務端進行分塊合併。
那麼對大檔案下載來說,我們能否採用類似的思想呢?在服務端支援 Range 請求首部的條件下,我們也是可以實現多執行緒分塊下載的功能,具體如下圖所示:
看完上圖相信你對大檔案下載的方案,已經有了一定的瞭解。接下來,我們先來介紹 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 範圍請求的相關知識就先介紹到這裡,下面我們步入正題開始介紹如何實現大檔案下載。
二、如何實現大檔案下載
為了讓大家能夠更好地理解後面的內容,我們先來看一下整體的流程圖:
瞭解完大檔案下載的流程之後,我們先來定義上述流程中涉及的一些輔助函式。
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 中如何利用 async-pool 這個庫提供的 asyncPool 函式,來實現大檔案的並行下載。除了介紹 asyncPool 函式之外,阿寶哥還介紹瞭如何通過 HEAD 請求獲取檔案大小、如何發起 HTTP 範圍請求及在客戶端如何儲存檔案等相關知識。其實利用 asyncPool 函式不僅可以實現大檔案的並行下載,而且還可以實現大檔案的並行上傳,感興趣的小夥伴可以自行嘗試一下。
以上就是JavaScript 中如何實現大檔案並行下載的詳細內容,更多關於JavaScript 大檔案並行下載的資料請關注我們其它相關文章!