先记住这个答案
fetch() 返回的 Promise 只关心响应,不暴露请求中间状态。规范未规定 Request body 发送时的进度回调。要获取上传进度,应改用 XMLHttpRequest:其 upload 对象会触发 progress 事件,提供 loaded 和 total。另一种思路是用 fetch 上传 ReadableStream 并包装流统计字节,但需要 duplex: 'half' 且无法获得网络层确认,只适合简单模拟。
- fetch 规范无上传进度事件
- XHR upload 对象有 progress 事件
- 流式方案只能近似统计
为什么 fetch 没有上传进度事件
fetch 规范只暴露请求创建和响应读取,并没有为上传阶段定义任何事件回调。规范中的 RequestInit.body 可以是字符串、FormData 或 ReadableStream,但发送过程由 HTTP 层管理,开发者无法订阅每块数据的发送结果。
即便把 body 设为 ReadableStream,fetch 内部会把流作为请求体管道输出,但规范没有规定向上传递发送进度。因此即便流中每个 chunk 被读取,也只能说明数据进入缓冲区,不代表被服务器接收,无法得到可靠百分比。
进度条需求下的技术选型
假设要上传一个 100MB 文件到对象存储,界面需要显示已传百分比。若使用 fetch,浏览器不提供任何事件源,只能靠自造流估算,而该估算与实际网络发送可能偏差较大。此时应改用 XMLHttpRequest,其 upload 对象上有 progress 事件。
实现时 new XMLHttpRequest(),open 对应地址,把 xhr.upload.onprogress 赋值,在该回调里读取 e.loaded 和 e.total,即可计算百分比。XHR 的 progress 事件由浏览器在收到上传数据确认时触发,相对准确,适合对进度有硬性要求的场景。
方案失效与处理
fetch 流式方案依赖 duplex: 'half',Chrome 105 才默认支持,旧浏览器无法使用;且统计的字节只是进入网络栈的量,并非服务端已接收的有效数据,若需精确到服务端收到,必须靠后端返回进度。
XHR 也不是万能:部分环境(如 Service Worker 内)没有 XMLHttpRequest;上传大文件时 progress 事件粒度受浏览器调度影响,可能超过数百毫秒才回调。替代时还需考虑断点续传等需求,必要时用双向流或 WebSocket。
容易答错的地方
- fetch 可以通过 response.body 读取到上传进度
- response.body 是响应体,与上传无关。上传进度只能在请求阶段获得,fetch 没有提供这类事件。正确做法是使用 XHR upload 事件或让服务端回传进度信息。
- ReadableStream 的 enqueue 计数就是上传进度
- 流读取只代表本地消费,不等于服务器确认。进度应反映网络层发送结果,stream 计数只能估算本地 CPU 处理量,无法替代真实上传进度。
面试官还会怎么问?
XHR 上传进度事件能取到 total 为 0 时怎么办?
e.lengthComputable 为 false 时无法计算百分比,常见于 chunked 编码。可改用 Content-Length 头或让后端返回任务 ID 以进度接口查询。
fetch 的 duplex: 'half' 有什么限制?
需要同时满足请求体是 ReadableStream 且请求不是 keepalive,必须显式指定这个选项才能发送流,而且无法获取实时上传进度。
Service Worker 中怎么获取上传进度?
Service Worker 中的 fetch 也没有进度事件,但可以使用 Request 的 body 作为流并自行观察字节,仍然不能得到真实网络发送进度,只能靠响应回执。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。