原文地址:https://javascript.info/fetch-progress,翻译时有删改
使用 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,但因为还没有被浏览器广泛支持(查看 browser issues),所以这里使用了 while 循环。
我们在循环里分块接收响应数据,知道加载结束,此时 done 属性值变 true。
为了记录进程,我们需要一个计数器,每个接收到一块数据 value 后,将其存储,并将其长度进入到计数器中。
下面展示了使用 fetch 方法监听下载进程的代码实现,更多的步骤细节可以在代码注释中看到。
// 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() 来获取流读取器(stream reader)。
- 需要注意的是,我们不能同时使用这两个方法来读取同一个响应对象,否则后一次使用会失败
- 在读取数据之前,我们能从响应头的 Content-Length 字段中,得到完整响应数据的长度
- 对跨域请求(查看 https://javascript.info/fetch-crossorigin 一章获取详情)可能没有这个字段,技术上讲,这个字段也没要求服务器必须设置,但通常会有的。
- 调用 await reader.read(),直到数据接收完成
- 我们使用数组 chunks 接收分块响应数据,这很重要,因为之后需要组装响应数据,而且后面我们也不能使用 response.json() 或其他什么方法处理了(你可以试下,会报错的)。
- 最后,我们得到了 chunks —— 它是一个 Uint8Array 类型数组,包含的是字节块数据。我们需要把这些数据合并到一块。不幸的是,现在单个方法能够做到直接合并,需要一点额外的代码来实现:
- 我们通过 chunksAll = new Uint8Array(receivedLength),创建了一个与总的接收长度一样长的类型数组
- 使用 .set(chunk, position) 方法,将 chunk 里的数据复制到 chunksAll 中去
- 现在得到了合并了数据之后的 chunksAll 变量,它还只是一个字节数据,而非字符串
- 为了得到字符串,我们需要解释 chunksAll 中的字节数据,通过内置的 TextDecoder 对象就可以。如果需要的话,可以再使用 JSON.parse 方法将字符串解析成对象。
- 如果我们需要的是二进制数据,怎么办呢?更简单了,第 4、第 5 步可以直接压缩成一句话,就能创建一个 Blob 对象。
最后,我们得到了结果(字符串或者 Blob,哪个方便用哪个),并且还实现了跟踪下载进度的功能。let blob = new Blob(chunks);
需要再辞提醒的是,现在还无法通过 fetch 方法监听上传进度,只能监听下载进度,如果有需求,请使用 XMLHttpRequest 来实现。
(完)