原文地址:https://javascript.info/fetch-progress,翻译时有删改

    现代JavaScript教程.png

    使用 fetch 方法可以跟踪下载进度。

    需要注意的是,现在还没有办法通过 fetch 方法跟踪上传进度。如果有需求,请使用 XMLHttpRequest 来实现,之后的章节中会有介绍。

    为了跟踪下载进度,我们需要用到 response.body 属性,这是一个 ReadableStream 对象,在 Streams API 规范中做了描述,提供了分段读取请求体数据的功能。

    与 response.text()、response.json() 这类方法不同的是,response.body 可以完全控制读取进程,我们还能随时计算已经传输了多少数据。

    下面是从 response.body 读取响应数据的一个代码例子:

    1. // instead of response.json() and other methods
    2. const reader = response.body.getReader();
    3. // infinite loop while the body is downloading
    4. while(true) {
    5. // done is true for the last chunk
    6. // value is Uint8Array of the chunk bytes
    7. const {done, value} = await reader.read();
    8. if (done) {
    9. break;
    10. }
    11. console.log(`Received ${value.length} bytes`)
    12. }

    await reader.read() 的返回结果是一个包含两个属性的对象:

    • done —— 数据读取完毕后,返回 true,否则返回 false
    • value —— 类型化字节数组:Uint8Array

    注意:

    Streams API 中也描述了使用异步迭代 for await..of 循环来遍历 ReadableStream,但因为还没有被浏览器广泛支持(查看 browser issues),所以这里使用了 while 循环。

    我们在循环里分块接收响应数据,知道加载结束,此时 done 属性值变 true。

    为了记录进程,我们需要一个计数器,每个接收到一块数据 value 后,将其存储,并将其长度进入到计数器中。

    下面展示了使用 fetch 方法监听下载进程的代码实现,更多的步骤细节可以在代码注释中看到。

    1. // Step 1: start the fetch and obtain a reader
    2. let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100');
    3. const reader = response.body.getReader();
    4. // Step 2: get total length
    5. const contentLength = +response.headers.get('Content-Length');
    6. // Step 3: read the data
    7. let receivedLength = 0; // received that many bytes at the moment
    8. let chunks = []; // array of received binary chunks (comprises the body)
    9. while(true) {
    10. const {done, value} = await reader.read();
    11. if (done) {
    12. break;
    13. }
    14. chunks.push(value);
    15. receivedLength += value.length;
    16. console.log(`Received ${receivedLength} of ${contentLength}`)
    17. }
    18. // Step 4: concatenate chunks into single Uint8Array
    19. let chunksAll = new Uint8Array(receivedLength); // (4.1)
    20. let position = 0;
    21. for(let chunk of chunks) {
    22. chunksAll.set(chunk, position); // (4.2)
    23. position += chunk.length;
    24. }
    25. // Step 5: decode into a string
    26. let result = new TextDecoder("utf-8").decode(chunksAll);
    27. // We're done!
    28. let commits = JSON.parse(result);
    29. alert(commits[0].author.login);

    下面,我们来一步步解释:

    1. 我们发起一个 fetch 请求,我们没有使用 response.json(),而是通过 response.body.getReader() 来获取流读取器(stream reader)。
      1. 需要注意的是,我们不能同时使用这两个方法来读取同一个响应对象,否则后一次使用会失败
    2. 在读取数据之前,我们能从响应头的 Content-Length 字段中,得到完整响应数据的长度
      1. 对跨域请求(查看 https://javascript.info/fetch-crossorigin 一章获取详情)可能没有这个字段,技术上讲,这个字段也没要求服务器必须设置,但通常会有的。
    3. 调用 await reader.read(),直到数据接收完成
      1. 我们使用数组 chunks 接收分块响应数据,这很重要,因为之后需要组装响应数据,而且后面我们也不能使用 response.json() 或其他什么方法处理了(你可以试下,会报错的)。
    4. 最后,我们得到了 chunks —— 它是一个 Uint8Array 类型数组,包含的是字节块数据。我们需要把这些数据合并到一块。不幸的是,现在单个方法能够做到直接合并,需要一点额外的代码来实现:
      1. 我们通过 chunksAll = new Uint8Array(receivedLength),创建了一个与总的接收长度一样长的类型数组
      2. 使用 .set(chunk, position) 方法,将 chunk 里的数据复制到 chunksAll 中去
    5. 现在得到了合并了数据之后的 chunksAll 变量,它还只是一个字节数据,而非字符串
      1. 为了得到字符串,我们需要解释 chunksAll 中的字节数据,通过内置的 TextDecoder 对象就可以。如果需要的话,可以再使用 JSON.parse 方法将字符串解析成对象。
      2. 如果我们需要的是二进制数据,怎么办呢?更简单了,第 4、第 5 步可以直接压缩成一句话,就能创建一个 Blob 对象。
        1. let blob = new Blob(chunks);
        最后,我们得到了结果(字符串或者 Blob,哪个方便用哪个),并且还实现了跟踪下载进度的功能。

    需要再辞提醒的是,现在还无法通过 fetch 方法监听上传进度,只能监听下载进度,如果有需求,请使用 XMLHttpRequest 来实现。

    (完)