2021 年 6 月 22 日

Fetch:下載進度

fetch 方法允許追蹤下載進度。

請注意:目前 fetch 無法追蹤上傳進度。為此,請使用 XMLHttpRequest,我們稍後會介紹。

若要追蹤下載進度,我們可以使用 response.body 屬性。它是一個 ReadableStream,一個特殊的物件,會逐塊提供主體,因為它會出現。可讀取串流在 串流 API 規範中進行說明。

response.text()response.json() 和其他方法不同,response.body 可以完全控制讀取程序,我們可以隨時計算消耗了多少。

以下是從 response.body 讀取回應的程式碼草稿

// instead of response.json() and other methods
const reader = response.body.getReader();

// infinite loop while the body is downloading
while(true) {
  // done is true for the last chunk
  // value is Uint8Array of the chunk bytes
  const {done, value} = await reader.read();

  if (done) {
    break;
  }

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

await reader.read() 呼叫的結果是一個具有兩個屬性的物件

  • done – 讀取完成時為 true,否則為 false
  • value – 位元組的類型化陣列:Uint8Array
請注意

Streams API 也描述了使用 for await..of 迴圈對 ReadableStream 進行非同步反覆運算,但它尚未廣泛支援(請參閱 瀏覽器問題),因此我們使用 while 迴圈。

我們在迴圈中接收回應區塊,直到載入完成,也就是說:直到 done 變成 true

若要記錄進度,我們只需要對每個接收到的片段 value 將其長度新增到計數器中。

以下是完整的實際範例,它會取得回應並在主控台中記錄進度,後續會有更多說明

// Step 1: start the fetch and obtain a 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: get total length
const contentLength = +response.headers.get('Content-Length');

// Step 3: read the data
let receivedLength = 0; // received that many bytes at the moment
let chunks = []; // array of received binary chunks (comprises the 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: concatenate chunks into single 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: decode into a string
let result = new TextDecoder("utf-8").decode(chunksAll);

// We're done!
let commits = JSON.parse(result);
alert(commits[0].author.login);

讓我們逐步說明

  1. 我們照常執行 fetch,但我們不會呼叫 response.json(),而是取得串流讀取器 response.body.getReader()

    請注意,我們無法使用這兩種方法來讀取相同的回應:請使用讀取器或回應方法來取得結果。

  2. 在讀取之前,我們可以從 Content-Length 標頭中找出完整的回應長度。

    對於跨來源要求(請參閱章節 Fetch:跨來源要求),它可能不存在,而且從技術上來說,伺服器不必設定它。但通常它會存在。

  3. 呼叫 await reader.read() 直到它完成。

    我們在陣列 chunks 中收集回應區塊。這很重要,因為在回應被使用後,我們將無法使用 response.json() 或其他方式「重新讀取」它(您可以試試看,會出現錯誤)。

  4. 最後,我們有 chunks – 一個 Uint8Array 位元組區塊陣列。我們需要將它們合併成單一結果。很遺憾地,沒有單一方法可以串接它們,因此有一些程式碼可以做到這一點

    1. 我們建立 chunksAll = new Uint8Array(receivedLength) – 一個具有合併長度的相同類型陣列。
    2. 然後使用 .set(chunk, position) 方法將每個 chunk 一個接一個地複製到其中。
  5. 我們在 chunksAll 中有結果。不過它是一個位元組陣列,而不是字串。

    若要建立字串,我們需要詮釋這些位元組。內建的 TextDecoder 正好可以做到這一點。然後我們可以 JSON.parse 它(如果需要的話)。

    如果我們需要二進位內容而不是字串,那就更簡單了。將步驟 4 和 5 替換為單一行,從所有區塊建立一個 Blob

    let blob = new Blob(chunks);

最後,我們有了結果(作為字串或 blob,無論哪一種都方便),並在過程中追蹤進度。

再次請注意,這不是用於上傳進度(現在無法使用fetch),僅用於下載進度。

此外,如果大小未知,我們應該在迴圈中檢查receivedLength,並在達到某個限制後中斷迴圈。這樣chunks才不會溢位記憶體。

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改善建議 - 請提交 GitHub 問題或提交拉取請求,而不是留言。
  • 如果您無法理解文章中的內容 - 請詳細說明。
  • 若要插入幾行程式碼,請使用<code>標籤,若要插入多行程式碼,請將程式碼包覆在<pre>標籤中,若要插入超過 10 行的程式碼,請使用沙盒 (plnkrjsbincodepen…)