示例说明
await res.arrayBuffer() 会把整个响应缓冲进内存,下载几 GB 文件必然崩溃。getReader 的手动循环每次只持有一个分块,调用 read 的时机完全由消费速度决定——下游处理慢,read 就调用得慢,TCP 窗口随之收缩,压力沿链路反向传导到服务端,这就是浏览器侧的背压机制。
技术要点
- reader 循环每次只驻留一个分块
- content-length 支撑百分比进度回调
- 消费延迟自动转化为上游读取减速
代码解读
循环里把每个 Uint8Array 存入 parts 累计已收字节;进度回调在总长已知时换算百分比;chunkPauseMs 模拟慢消费者,验证读取节奏随之放慢。结束后 Blob 组装全部分块,received 与 total 对账校验完整性。
使用建议
真正落盘场景应配合流式写入而非累积 Blob。中途取消调用 reader.cancel 让连接释放。进度回调避免过度频繁触发 UI 更新,可按阈值节流。
代码示例
// stream the response body incrementally so backpressure applies
async function downloadWithProgress(url, onProgress, chunkPauseMs = 0) {
const res = await fetch(url);
if (!res.ok || !res.body) throw new Error('download failed: ' + res.status);
const total = Number(res.headers.get('content-length') ?? 0);
const reader = res.body.getReader();
let received = 0;
const parts = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
parts.push(value);
received += value.length;
if (onProgress && total > 0) {
onProgress(received, total);
}
if (chunkPauseMs > 0) {
// 消费变慢时读取循环自然放慢,读取暂停即向上游传导背压
await new Promise((r) => setTimeout(r, chunkPauseMs));
}
}
const blob = new Blob(parts);
return { bytes: received, total, blob };
}
const result = await downloadWithProgress(
'https://cdn.example.com/dataset.bin',
(got, total) => console.log('progress:', Math.round((got / total) * 100) + '%'),
10,
);
console.log('downloaded', result.bytes, 'of', result.total, 'bytes');