「譯」六步實現JavaScript大型檔案並行下載?

寫在前面的話

我會將過去幾年學到的 Canvas 製圖理論、實踐進行濃縮、並編輯成冊。希望透過體系化的內容組織助你快速入門、深入理解 Canvas。當然,因篇幅有限,本課程可能無法做到面面俱到,但是有了核心知識的積累,不論是繼續閱讀 Canvas 系列書籍,還是進一步擴充套件 Canvas 的學習領域、如 3D 製圖等,都能做到左右逢源。

本 Canvas 系列課程已經編輯成冊並陸續更新,下面是已更新章節傳送門:

《Canvas 自動化製圖必知必會-導讀篇》

《Canvas可視區與虛擬畫布》

《Canvas 上下文詳解》

《一文讀懂 Canvas 中的 scale 與 translate》

《一文讀懂 Canvas 中 rotate 與 skew 操作》

《Canvas 矩陣映象那些你不得不知的數學原理》

話不多少,直接進入正題。

前言

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

var blob = instanceOfBlob。slice([start [, end [, contentType]]]};

備註: 在某些瀏覽器和版本上具有供應商字首:例如:Firefox 12 及更早版本的 blob。mozSlice() 和 Safari 中的 blob。webkitSlice()。 slice() 方法的舊版本,沒有供應商字首,具有不同的語義,並且已過時。

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

「譯」六步實現JavaScript大型檔案並行下載?

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

1。HTTP Range 請求

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

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

Range 語法

Range: =-Range: =-Range: =--Range: =---

unit:Range 請求所採用的單位,通常是位元組(bytes)

:一個整數,表示在特定單位下,Range 的起始值

:一個整數,表示在特定單位下,Range 的結束值。這個值是可選的,如果不存在,表示此 Range 一直延伸到文件結束。

單一 Range

curl https://www。baidu。com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf。png -i -H “Range: bytes=0-1023”

輸出結果如下:

「譯」六步實現JavaScript大型檔案並行下載?

多重 Range

curl http://www。baidu。com -i -H “Range: bytes=0-50, 100-150”

輸出結果如下:

「譯」六步實現JavaScript大型檔案並行下載?

2 HTTP Range 大檔案下載

2。1 定義輔助函式

2。1。1 getContentLength 函式

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

function getContentLength(url) { return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr。open(‘HEAD’, url); // 傳送HEAD請求 xhr。send(); xhr。onload = function () { resolve(~~xhr。getResponseHeader(‘Content-Length’)); // 獲取檔案長度 }; xhr。onerror = reject; });}

2。1。2 asyncPool 函式

asyncPool 函式用於實現非同步任務的併發控制。該函式接收 3 個引數:

poolLimit(數字型別):表示限制的併發數

array(陣列型別):表示任務陣列;

iteratorFn(函式型別):表示迭代函式,用於實現對每個任務項進行處理,該函式會返回一個 Promise 物件或非同步函式。

async function asyncPool(poolLimit, array, iteratorFn) { const ret = []; // 儲存所有的非同步任務 const executing = []; // 儲存正在執行的非同步任務 for (const item of array) { const p = Promise。resolve()。then(() => iteratorFn(item, array)); ret。push(p); if (poolLimit <= array。length) { const e = p。then(() => executing。splice(executing。indexOf(e), 1)); executing。push(e); if (executing。length >= poolLimit) { // 等待較快的任務執行完成 await Promise。race(executing); } } } return Promise。all(ret);}

2。1。3 getBinaryContent 函式

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

function getBinaryContent(url, start, end, i) { return new Promise((resolve, reject) => { try { let xhr = new XMLHttpRequest(); xhr。open(‘GET’, url, true); xhr。setRequestHeader(‘range’, `bytes=${start}-${end}`); // 請求頭上設定Range請求資訊 xhr。responseType = ‘arraybuffer’; // 設定返回的型別為arraybuffer xhr。onload = function () { resolve({ index: i, // 檔案塊的索引 buffer: xhr。response, // Range請求對應的資料 }); }; xhr。send(); } catch (err) { reject(new Error(err)); } });}

需要注意的是

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

2。1。4 concatenate 函式

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

function concatenate(arrays) { if (!arrays。length) return null; let totalLength = arrays。reduce((acc, value) => acc + value。length, 0); let result = new Uint8Array(totalLength); let length = 0; for (let array of arrays) { result。set(array, length); length += array。length; } return result;}

2。1。5 saveAs 函式

saveAs 函式用於實現客戶端檔案儲存的功能,這裡只是一個簡單的實現。在實際專案中,可以考慮直接使用 FileSaver。js,具體使用可以閱讀文末參考文獻。

function saveAs({ name, buffers, mime = ‘application/octet-stream’ }) { const blob = new Blob([buffers], { type: mime }); // 建立Blob const blobUrl = URL。createObjectURL(blob); // 例項化 const a = 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:/,對應的示例如下:

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

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

2。1。6 定義 download 函式

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

url(字串型別):預下載資源的地址

chunkSize(數字型別):分塊的大小,單位為位元組

poolLimit(數字型別):表示限制的併發數

async function download({ url, chunkSize, poolLimit = 1 }) { const contentLength = await getContentLength(url); const chunks = typeof chunkSize === ‘number’ ? Math。ceil(contentLength / chunkSize) : 1; const results = await asyncPool( poolLimit, [。。。new Array(chunks)。keys()], (i) => { let start = i * chunkSize; let end = i + 1 == chunks ? contentLength - 1 : (i + 1) * chunkSize - 1; return getBinaryContent(url, start, end, i); } ); const sortedBuffers = results。map((item) => new Uint8Array(item。buffer)); return concatenate(sortedBuffers);}

2。2 大檔案下載使用示例

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

function multiThreadedDownload() { const url = document。querySelector(‘#fileUrl’)。value; if (!url || !/https?/。test(url)) return; console。log(‘multi threaded download start: ’ + +new Date()); download({ url, chunkSize: 0。1 * 1024 * 1024, poolLimit: 6, })。then((buffers) => { console。log(‘multi threaded download end: ’ + +new Date()); saveAs({ buffers, name: ‘myzip’, mime: ‘application/zip’ }); });}

完整程式碼請檢視文末參考文獻。

3。總結

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

參考資料

https://blog。bitsrc。io/implement-concurrent-download-of-large-files-in-javascript-4e94202c5373

https://github。com/eligrey/FileSaver。js

https://mp。weixin。qq。com/s/lQKTCS_QB0E62SK9oXD4LA

https://gist。github。com/semlinker/837211c039e6311e1e7629e5ee5f0a42

https://juejin。cn/post/69548688790341

https://developer。mozilla。org/zh-CN/docs/Web/API/Blob/slice