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);
让我们一步一步地解释。
-
我们像往常一样执行 `fetch`,但不是调用 `response.json()`,而是获取一个流读取器 `response.body.getReader()`。
请注意,我们不能使用这两种方法来读取同一个响应:要么使用读取器,要么使用响应方法来获取结果。
-
在读取之前,我们可以从 `Content-Length` 头部确定完整的响应长度。
对于跨域请求,它可能不存在(参见章节 Fetch: 跨域请求),并且,从技术上讲,服务器不必设置它。但通常情况下它都在那里。
-
调用 `await reader.read()` 直到完成。
我们将响应块收集到数组 `chunks` 中。这很重要,因为在响应被消耗后,我们将无法使用 `response.json()` 或其他方法“重新读取”它(你可以尝试,会有错误)。
-
最后,我们得到了 `chunks` - 一个 `Uint8Array` 字节块数组。我们需要将它们合并成一个结果。不幸的是,没有一个方法可以将它们连接起来,所以需要一些代码来完成这个操作。
- 我们创建 `chunksAll = new Uint8Array(receivedLength)` - 一个具有组合长度的相同类型数组。
- 然后使用 `.set(chunk, position)` 方法将每个 `chunk` 逐个复制到其中。
-
我们在 `chunksAll` 中得到了结果。它是一个字节数组,而不是字符串。
要创建字符串,我们需要解释这些字节。内置的 TextDecoder 恰好可以做到这一点。然后,如果需要,我们可以对其进行 `JSON.parse`。
如果我们需要二进制内容而不是字符串呢?这更简单。用一行代码替换步骤 4 和 5,从所有块创建
Blob
let blob = new Blob(chunks);
最后我们得到了结果(作为字符串或 Blob,无论哪种更方便),以及过程中的进度跟踪。
再次注意,这不是用于上传进度(现在无法使用 fetch
),仅用于下载进度。
此外,如果大小未知,我们应该在循环中检查 receivedLength
,并在其达到一定限制时中断。这样 chunks
不会溢出内存。
评论
<code>
标签,对于多行代码,请将其包装在<pre>
标签中,对于超过 10 行的代码,请使用沙箱(plnkr,jsbin,codepen…)