2021年6月22日

获取:下载进度

fetch 方法允许跟踪下载进度。

请注意:目前fetch无法跟踪上传进度。为此,请使用XMLHttpRequest,我们将在后面介绍。

要跟踪下载进度,我们可以使用response.body属性。它是一个ReadableStream – 一个特殊的对象,它按块提供主体,就像它到来一样。可读流在Streams 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…)