Javascript Fetch:下載進(jìn)度

2023-02-17 10:57 更新

?fetch? 方法允許去跟蹤 下載 進(jìn)度。

請(qǐng)注意:到目前為止,fetch 方法無(wú)法跟蹤 上傳 進(jìn)度。對(duì)于這個(gè)目的,請(qǐng)使用 XMLHttpRequest,我們?cè)诤竺嬲鹿?jié)會(huì)講到。

要跟蹤下載進(jìn)度,我們可以使用 response.body 屬性。它是 ReadableStream —— 一個(gè)特殊的對(duì)象,它可以逐塊(chunk)提供 body。在 Streams API 規(guī)范中有對(duì) ReadableStream 的詳細(xì)描述。

與 response.text()response.json() 和其他方法不同,response.body 給予了對(duì)進(jìn)度讀取的完全控制,我們可以隨時(shí)計(jì)算下載了多少。

這是從 response.body 讀取 response 的示例代碼:

// 代替 response.json() 以及其他方法
const reader = response.body.getReader();

// 在 body 下載時(shí),一直為無(wú)限循環(huán)
while(true) {
  // 當(dāng)最后一塊下載完成時(shí),done 值為 true
  // value 是塊字節(jié)的 Uint8Array
  const {done, value} = await reader.read();

  if (done) {
    break;
  }

  console.log(`Received ${value.length} bytes`)
}

await reader.read() 調(diào)用的結(jié)果是一個(gè)具有兩個(gè)屬性的對(duì)象:

  • ?done? —— 當(dāng)讀取完成時(shí)為 ?true?,否則為 ?false?。
  • ?value? —— 字節(jié)的類型化數(shù)組:?Uint8Array?。

請(qǐng)注意:

Streams API 還描述了如果使用 for await..of 循環(huán)異步迭代 ReadableStream,但是目前為止,它還未得到很好的支持(參見(jiàn) 瀏覽器問(wèn)題),所以我們使用了 while 循環(huán)。

我們?cè)谘h(huán)中接收響應(yīng)塊(response chunk),直到加載完成,也就是:直到 done 為 true。

要將進(jìn)度打印出來(lái),我們只需要將每個(gè)接收到的片段 value 的長(zhǎng)度(length)加到 counter 即可。

這是獲取響應(yīng),并在控制臺(tái)中記錄進(jìn)度的完整工作示例,下面有更多說(shuō)明:

// Step 1:?jiǎn)?dòng) fetch,并獲得一個(gè) reader
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100');

const reader = response.body.getReader();

// Step 2:獲得總長(zhǎng)度(length)
const contentLength = +response.headers.get('Content-Length');

// Step 3:讀取數(shù)據(jù)
let receivedLength = 0; // 當(dāng)前接收到了這么多字節(jié)
let chunks = []; // 接收到的二進(jìn)制塊的數(shù)組(包括 body)
while(true) {
  const {done, value} = await reader.read();

  if (done) {
    break;
  }

  chunks.push(value);
  receivedLength += value.length;

  console.log(`Received ${receivedLength} of ${contentLength}`)
}

// Step 4:將塊連接到單個(gè) Uint8Array
let chunksAll = new Uint8Array(receivedLength); // (4.1)
let position = 0;
for(let chunk of chunks) {
  chunksAll.set(chunk, position); // (4.2)
  position += chunk.length;
}

// Step 5:解碼成字符串
let result = new TextDecoder("utf-8").decode(chunksAll);

// 我們完成啦!
let commits = JSON.parse(result);
alert(commits[0].author.login);

讓我們一步步解釋下這個(gè)過(guò)程:

  1. 我們像往常一樣執(zhí)行 fetch,但不是調(diào)用 response.json(),而是獲得了一個(gè)流讀取器(stream reader)response.body.getReader()。
  2. 請(qǐng)注意,我們不能同時(shí)使用這兩種方法來(lái)讀取相同的響應(yīng)。要么使用流讀取器,要么使用 reponse 方法來(lái)獲取結(jié)果。

  3. 在讀取數(shù)據(jù)之前,我們可以從 Content-Length header 中得到完整的響應(yīng)長(zhǎng)度。
  4. 跨源請(qǐng)求中可能不存在這個(gè) header(請(qǐng)參見(jiàn) Fetch:跨源請(qǐng)求),并且從技術(shù)上講,服務(wù)器可以不設(shè)置它。但是通常情況下它都會(huì)在那里。

  5. 調(diào)用 await reader.read(),直到它完成。
  6. 我們將響應(yīng)塊收集到數(shù)組 chunks 中。這很重要,因?yàn)樵谑褂猛辏╟onsumed)響應(yīng)后,我們將無(wú)法使用 response.json() 或者其他方式(你可以試試,將會(huì)出現(xiàn) error)去“重新讀取”它。

  7. 最后,我們有了一個(gè) chunks —— 一個(gè) Uint8Array 字節(jié)塊數(shù)組。我們需要將這些塊合并成一個(gè)結(jié)果。但不幸的是,沒(méi)有單個(gè)方法可以將它們串聯(lián)起來(lái),所以這里需要一些代碼來(lái)實(shí)現(xiàn):
    1. 我們創(chuàng)建 ?chunksAll = new Uint8Array(receivedLength)? —— 一個(gè)具有所有數(shù)據(jù)塊合并后的長(zhǎng)度的同類型數(shù)組。
    2. 然后使用 ?.set(chunk, position)? 方法,從數(shù)組中一個(gè)個(gè)地復(fù)制這些 ?chunk?。
  8. 我們的結(jié)果現(xiàn)在儲(chǔ)存在 chunksAll 中。但它是一個(gè)字節(jié)數(shù)組,不是字符串。
  9. 要?jiǎng)?chuàng)建一個(gè)字符串,我們需要解析這些字節(jié)??梢允褂脙?nèi)建的 TextDecoder 對(duì)象完成。然后,我們可以 JSON.parse 它,如果有必要的話。

    如果我們需要的是二進(jìn)制內(nèi)容而不是字符串呢?這更簡(jiǎn)單。用下面這行代碼替換掉第 4 和第 5 步,這行代碼從所有塊創(chuàng)建一個(gè) Blob

    let blob = new Blob(chunks);
    

最后,我們得到了結(jié)果(以字符串或 blob 的形式表示,什么方便就用什么),并在過(guò)程中對(duì)進(jìn)度進(jìn)行了跟蹤。

再?gòu)?qiáng)調(diào)一遍,這不能用于 上傳 過(guò)程(現(xiàn)在無(wú)法通過(guò) fetch 獲取),僅用于 下載 過(guò)程。

另外,如果大小未知,我們應(yīng)該檢查循環(huán)中的 receivedLength,一旦達(dá)到一定的限制就將其中斷。這樣 chunks 就不會(huì)溢出內(nèi)存了。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)