Fetch API下载进度指示器?

21

我正在尝试捕获Fetch请求的下载进度,并将其用于改变进度条的宽度。我查看了ProgressEvent.lengthComputable作为潜在解决方案,但不确定是否可以与Fetch API一起使用。


1
不是这样的。fetch() 的 Promise 在收到第一个数据包后就会解析,但并不会等到整个响应体都到达才解析。 - Touffy
1
https://dev59.com/j1oV5IYBdhLWcg3wRtHa - Adrian
1
如果需要进度指示器,那么https://dev59.com/elsV5IYBdhLWcg3w2h4s可能更好,而且它比较老。 - skyboyer
非常感谢Adriani6和Touffy提供的信息。 - skyboyer
6
因为这个问题是关于下载的特定问题,建议的重复回答与上传有关,所以让我们重新开启此问题。 - anthumchris
显示剩余2条评论
3个回答

23
没有检查错误(如try / catch等)

const elStatus = document.getElementById('status');
function status(text) {
  elStatus.innerHTML = text;
}

const elProgress = document.getElementById('progress');
function progress({loaded, total}) {
  elProgress.innerHTML = Math.round(loaded/total*100)+'%';
}

async function main() {
  status('downloading with fetch()...');
  const response = await fetch('https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg');
  const contentLength = response.headers.get('content-length');
  const total = parseInt(contentLength, 10);
  let loaded = 0;

  const res = new Response(new ReadableStream({
    async start(controller) {
      const reader = response.body.getReader();
      for (;;) {
        const {done, value} = await reader.read();
        if (done) break;
        loaded += value.byteLength;
        progress({loaded, total})
        controller.enqueue(value);
      }
      controller.close();
    },
  }));
  const blob = await res.blob();
  status('download completed')
  document.getElementById('img').src = URL.createObjectURL(blob);
}

main();
<div id="status">&nbsp;</div>
<h1 id="progress">&nbsp;</h1>
<img id="img" />

参考自此处


1
如果服务器发送压缩编码响应,则此方法无效。例如,gzip 压缩。假设客户端发送 Accept-Encoding: gzip 标头,服务器响应如下 - Content-Type: application/json Content-Encoding: gzip Content-Length: xxx那么,在从 body reader 读取时,长度 xxx 将比块的总长度小得多。基本上,在某个点之后,loaded 将比 total 多。因为 content-length 标头包含压缩响应的大小。但是,loaded 是解压缩后计算的块大小。 - ecthiender
看起来你在压缩方面运气不佳。没有标准的头部可以发送压缩数据的大小。如果你控制服务器,你可以发送一个自定义头部来包含这些信息。 - gman
进度在内容使用gzip编码时不正确:https://github.com/AnthumChris/fetch-progress-indicators/issues/13 / gzip编码响应的进度不正确:https://github.com/samundrak/fetch-progress/issues/22 - Mir-Ismaili

4
使用此工具:
async function* streamAsyncIterable(stream) {
  const reader = stream.getReader()
  try {
    while (true) {
      const { done, value } = await reader.read()
      if (done) return
      yield value
    }
  } finally {
    reader.releaseLock()
  }
}

参见:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for-await...of#iterating_over_async_generators

然后,您可以使用 for await...of 循环:

const response = await fetch(url)
let responseSize = 0
for await (const chunk of streamAsyncIterable(response.body)) {
  responseSize += chunk.length
}

但请注意,responseSize响应大小!并不一定是下载大小!有什么区别?如果没有 content-encodinggzipbr,...),则没有区别。但如果应用了压缩,则最终的下载大小将是压缩数据的大小(相同的 content-length),而最终的响应大小将是未压缩数据的大小。
请参见 @ecthiender 的评论此线程

这似乎是一个很好的答案。我有一个问题。我尝试在后面调用foo = await response.json(),但失败了,因为body流已经被读取。有没有办法在最后从响应中获取json? - dooderson
3
@dooderson; 据我所知,在这种情况下,您需要手动完成。我的意思是您需要在相同的for await循环中读取(和连接)totalBytes。然后,您需要将totalBytes转换为string,然后再进行JSON.parse()。不要将每个chunk都转换为string(然后连接string)!这会导致一些问题,因为多字节字符可能位于两个连续块的边界处。 - Mir-Ismaili

-4

你可以使用axios代替

import axios from 'axios'
export async function uploadFile(file, cb) {
  const url = `//127.0.0.1:4000/profile`
  try {
    let formData = new FormData()
    formData.append("avatar", file)
    const data = await axios.post(url, formData, {
      onUploadProgress: (progressEvent) => {
        console.log(progressEvent)
        if (progressEvent.lengthComputable) {
          let percentComplete = progressEvent.loaded / progressEvent.total;
          if (cb) {
            cb(percentComplete)
          }
        }
      }
    })
    return data
  } catch (error) {
    console.error(error)
  }
}

10
要求使用FETCH而不是xmlHTTP! - jahajee.com
@jahajee.com 你这样的评论正是我们将被智能化取代的原因。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接