Axios 进度捕获实战:onUploadProgress 与 onDownloadProgress 在浏览器和 Node.js 中的实现原理
本文基于 axios 官方文档 Progress capturing 展开,讲解如何在浏览器与 Node.js 两种环境中捕获上传/下载进度事件、理解 AxiosProgressEvent 各字段的含义与默认值,并结合 进度事件归约器、节流工具 与 xhr、http、fetch 三个适配器的源码,深入剖析"每秒 3 次"的节流机制、速率/剩余时间估算算法,以及 Node.js 下流式上传时 maxRedirects: 0 的必要性。读完本文,你可以完整掌握 axios 进度回调的接入方式、字段语义和底层触发链路,并能在真实项目中正确实现大文件上传/下载的进度条。
进度事件能提供什么:AxiosProgressEvent 字段全解
axios 在浏览器和 Node 环境中都支持捕获请求的上传/下载进度。官方文档给出的最小可用示例是:
await axios.post(url, data, {
onUploadProgress: function (axiosProgressEvent) {
/*{
loaded: number;
total?: number;
progress?: number; // in range [0..1]
bytes: number; // how many bytes have been transferred since the last trigger (delta)
estimated?: number; // estimated time in seconds
rate?: number; // upload speed in bytes
upload: true; // upload sign
}*/
},
onDownloadProgress: function (axiosProgressEvent) {
/*{
loaded: number;
total?: number;
progress?: number;
bytes: number;
estimated?: number;
rate?: number; // download speed in bytes
download: true; // download sign
}*/
},
});
结合 TypeScript 类型定义 index.d.ts 中的 AxiosProgressEvent 接口(L363-L374)与源码 progressEventReducer.js 中实际构造的对象,完整的字段语义如下:
| 字段 | 类型 | 必选 | 含义与说明 |
|---|---|---|---|
loaded |
number | 是 | 当前已传输的字节数。源码会将其钳制在 [0, total] 区间,避免个别环境下 loaded 越界 |
total |
number | 否 | 总字节数。仅当传输方"可计算长度"(如设置了 Content-Length / 响应带 Content-Length)时才存在 |
progress |
number | 否 | 进度比例,取值范围 [0..1],等于 loaded / total;没有 total 时该字段为 undefined |
bytes |
number | 是 | 自上次触发以来新传输的字节数(增量 delta),不是累计值 |
rate |
number | 否 | 传输速率,单位 bytes/秒,由速率计(speedometer)计算得出 |
estimated |
number | 否 | 按当前速率估算的剩余时间(秒),等于 (total - loaded) / rate;rate 或 total 缺失时无此字段 |
upload |
boolean | 否 | 上传标记。仅上传进度事件携带 upload: true |
download |
boolean | 否 | 下载标记。仅下载进度事件携带 download: true |
event |
any | 否 | 原始底层事件对象(浏览器为原生 ProgressEvent,Node 下为装饰后的事件),类型定义中记为 BrowserProgressEvent |
lengthComputable |
boolean | 是 | 本次事件能否计算总长,即 total != null 的布尔结果 |
可以看到,官方文档注释里的字段是核心子集,而类型定义与源码还额外暴露了 event 和 lengthComputable,方便在回调里判断"是否处于可计算进度状态"。
核心机制之一:强制 3 次/秒的节流
官方文档明确指出:"The frequency of progress events is forced to be limited to 3 times per second. This is to prevent the browser from being overwhelmed with progress events."(进度事件频率被强制限制为每秒 3 次,以防浏览器被进度事件淹没。)
这一限制在 progressEventReducer.js 中实现:
export const progressEventReducer = (listener, isDownloadStream, freq = 3) => {
let bytesNotified = 0;
const _speedometer = speedometer(50, 250);
return throttle((e) => {
// ... 计算 loaded / total / bytes / rate / estimated 并调用 listener(data)
}, freq);
};
三个关键点:
- 默认频率
freq = 3,且三个适配器(xhr/http/fetch)在 Node 侧均以字面量3显式传入,浏览器侧使用默认值。也就是说,每秒 3 次是硬性约定,不暴露配置项。 - 节流不是简单丢弃。throttle.js 返回一个二元组
[throttled, flush]:throttled在时间阈值(1000 / freq毫秒)内合并调用、只保留最新参数,并挂一个setTimeout延迟补发;flush则立即把最后一次被合并的参数"冲刷"出去。适配器在上传流结束时调用flush(见下文),保证进度条最终停在 100%,而不是停在最后一次节流时刻。 - 增量
bytes由bytesNotified维护:progressBytes = max(0, loaded - bytesNotified),bytesNotified单调不减,即使个别环境事件乱序或回退也不会出现负增量。
浏览器 xhr 适配器中的接线方式见 xhr.js L199-L212:
// Handle progress if needed
if (onDownloadProgress) {
[downloadThrottled, flushDownload] = progressEventReducer(onDownloadProgress, true);
request.addEventListener('progress', downloadThrottled);
}
// Not all browsers support upload events
if (onUploadProgress && request.upload) {
[uploadThrottled, flushUpload] = progressEventReducer(onUploadProgress);
request.upload.addEventListener('progress', uploadThrottled);
request.upload.addEventListener('loadend', flushUpload);
}
注意两个细节:
- 下载进度监听在
XMLHttpRequest本体的progress事件上(isDownloadStream = true,因此事件带download: true标记); - 上传进度依赖
request.upload(Upload对象),源码用request.upload的存在性做了特性探测——"Not all browsers support upload events"; loadend时执行flushUpload,把最后一次被节流合并的上传事件强制补发,保证收尾事件不丢失。
核心机制之二:速率与剩余时间估算(speedometer)
rate 和 estimated 字段由 speedometer.js 提供。它的构造参数在 progressEventReducer 中固定为 speedometer(50, 250):
samplesCount = 50:内部是一个环形缓冲,保存最近 50 个样本(每次push记录一个"自上次触发以来的字节数"和时间戳);min = 250:采样窗口不足 250 毫秒时不返回速率(if (now - firstSampleTS < min) return;),避免传输刚开始、样本太少时出现剧烈抖动的假速率。
一旦满足最小窗口,速率按 Math.round((bytesCount * 1000) / passed) 计算,即"窗口内累计字节数 / 窗口经过的毫秒数 × 1000",单位 bytes/s。随后 estimated = (total - loaded) / rate 直接由 progressEventReducer 组合得出。这也解释了为什么文档中 rate、estimated 均为可选字段:冷启动阶段或 total 未知时它们就是 undefined。
Node.js 环境:流式上传与进度事件
官方文档的第二段展示了 Node.js 中把上传进度事件"流式化"消费的典型场景——上传一个可读流并实时打印百分比:
const { data } = await axios.post(SERVER_URL, readableStream, {
onUploadProgress: ({ progress }) => {
console.log((progress * 100).toFixed(2));
},
headers: {
"Content-Length": contentLength,
},
maxRedirects: 0, // avoid buffering the entire stream
});
这段示例有三个实操要点,全部与源码对应:
-
手动声明
Content-Length才能算出progress。progress = loaded / total,而total来自请求的Content-Length头。Node 侧 http 适配器在 http.js L845、L874-L878 中正是用utils.toFiniteNumber(headers.getContentLength())取出contentLength,再交给progressEventDecorator包装:const contentLength = utils.toFiniteNumber(headers.getContentLength()); // ... onUploadProgress && data.on( 'progress', flushOnFinish( data, progressEventDecorator( contentLength, progressEventReducer(asyncDecorator(onUploadProgress, scheduleProgress), false, 3) ) ) );不声明该头时
total为undefined,回调里只有loaded、bytes可用,progress为undefined。 -
上传数据被包装为带
progress事件的流管道。Node 侧并非直接"监听字节数",而是把请求体经过stream.pipeline([data, new AxiosTransformStream({ maxRate })])(http.js L859-L867),由转换流按块发出progress事件,再经asyncDecorator(onUploadProgress, scheduleProgress)调度到异步回调中执行,避免在流事件循环中阻塞事件循环。 -
maxRedirects: 0强烈建议保留。文档中的危险提示原文是:It is recommended to disable redirects by setting maxRedirects: 0 to upload the stream in the node.js environment, as the follow-redirects package will buffer the entire stream in RAM without following the "backpressure" algorithm.
即:Node 的 http 适配器处理重定向依赖
follow-redirects,该包在"重传同一个请求体"时会把整个流缓冲进内存,且不遵守 backpressure(背压)算法——对大文件流式上传意味着内存可能被撑爆。若目标地址不会发生重定向(例如自己控制的服务端),设maxRedirects: 0可以让流真正逐块发送。下载侧同理:onDownloadProgress的进度来自响应流上串联的AxiosTransformStream(http.js L1125-L1147),total取自响应头content-length。
环境差异与限制
文档中还有一个必须知道的限制(warning 级别):
Capturing FormData upload progress is not currently supported in node.js environments.
即在 Node.js 环境中,基于 FormData 的上传目前无法捕获进度。从源码结构看,这与 Node 侧进度依赖 stream.Readable 包装链路有关:http 适配器对非流数据会执行 stream.Readable.from(data),而 Node 的 FormData 体本身并不经过这条发出 progress 事件的管道,因此拿不到增量字节数。如果你的 Node 场景需要 FormData 上传 + 进度条,可行的替代思路是自行构造 multipart/form-data 的可读流(同时手动设置 Content-Length),而不是直接传 FormData 实例。
fetch 适配器同样支持进度:上传侧通过 trackRequestStream(_request.body, onProgress, flush)(fetch.js L380-L390)跟踪请求体流,下载侧在 L526-L528 用 progressEventDecorator + progressEventReducer(asyncDecorator(onDownloadProgress), true) 包装响应流,且都使用 asyncDecorator 把回调调度到微任务/异步队列,与 http 适配器保持同一套事件语义。
相关配置与测试验证
- 速率上限:
index.d.ts中定义了maxUploadRate/maxDownloadRate(type MaxUploadRate = number),http 适配器会把maxRate配置(可为[upload, download]数组)交给AxiosTransformStream({ maxRate })(http.js L847-L864),用于在限速的同时仍能产出进度事件——onUploadProgress || maxUploadRate满足其一即建立进度管道。 - 浏览器端行为测试:tests/browser/progress.browser.test.js 覆盖了 xhr 适配器的上/下载进度与节流场景。
- Node 端行为测试:tests/unit/adapters/http.test.js(L4958、L5027、L5137 等多处)逐一断言了
loaded、total、progress、bytes、rate、upload/download各字段的实际值;tests/unit/adapters/fetch.test.js 验证 fetch 适配器,tests/smoke/esm/tests/progress.smoke.test.js 等 smoke 测试覆盖跨环境的基本链路。
小结
- 进度捕获通过请求配置中的
onUploadProgress/onDownloadProgress接入,事件对象在三种适配器(xhr / http / fetch)中语义一致; - 事件频率被
progressEventReducer强制节流到 3 次/秒,并用throttle的flush机制保证收尾事件不丢失; progress、rate、estimated均为可选字段,取决于Content-Length是否存在与采样窗口是否达到 250ms;- Node.js 流式上传建议显式声明
Content-Length并设置maxRedirects: 0,避免 follow-redirects 整流缓冲;Node 环境下FormData上传暂不支持进度捕获。
以上机制均可在 lib/helpers/progressEventReducer.js、lib/helpers/throttle.js、lib/helpers/speedometer.js 与三个适配器源码中直接查证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00