axios 进度捕获完全指南:onUploadProgress / onDownloadProgress 的实现原理与源码剖析
axios 在浏览器与 Node.js 双环境下都支持捕获上传/下载进度,这是实现上传进度条、下载速度显示等交互功能的基础能力。本文将完整覆盖进度事件的配置用法、事件对象各字段含义、Node.js 流式上传的注意事项,并深入 lib/helpers 下的节流器与速率表源码,讲清"为什么进度事件被限制为每秒 3 次"以及 progress、rate、estimated 等字段是如何计算出来的。
一、进度捕获的核心机制
axios 同时支持在浏览器和 Node.js 环境中捕获请求的上传与下载进度。官方文档 docs/pages/advanced/progress-capturing.md(西班牙语版本见 docs/es/pages/advanced/progress-capturing.md)明确指出:进度事件的触发频率被强制限制为每秒 3 次,目的是防止浏览器被大量进度事件淹没、拖垮主线程。
这一"3 次/秒"并非配置项,而是硬编码在事件归约器中的默认频率。从 lib/helpers/progressEventReducer.js 的源码结构看,其签名即为 progressEventReducer(listener, isDownloadStream, freq = 3),三个适配器(xhr / http / fetch)在接线时均使用该默认值,Node.js 的 lib/adapters/http.js 中甚至显式传入 3。
配置层面,进度回调在请求配置中通过两个可选字段挂载,对应 TypeScript 定义见 index.d.ts:
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
在 lib/core/mergeConfig.js 中,这两个字段采用 defaultToConfig2 合并策略——即请求级配置优先于实例级默认配置,你可以在 axios.create 时设置全局默认回调,再在单次请求中覆盖。
二、基本用法:捕获上传与下载进度
官方文档给出的标准用法如下,直接对 post 请求配置两个回调即可:
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
}*/
},
});
事件对象字段详解
axiosProgressEvent 即 TS 中的 AxiosProgressEvent 接口(index.d.ts)。结合文档注释与源码,各字段含义如下:
| 字段 | 类型 | 是否可选 | 说明 |
|---|---|---|---|
loaded |
number | 必填 | 已传输的字节数。源码中会先做钳制:Math.max(0, Math.min(rawLoaded, total)),保证 loaded 不会超过 total(见 progressEventReducer.js) |
total |
number | 可选 | 总字节数。只有服务端/环境提供了 Content-Length(浏览器中对应 lengthComputable === true)时才有值 |
progress |
number | 可选 | 进度比率,取值范围 [0..1],由 loaded / total 计算;无 total 时为 undefined |
bytes |
number | 必填 | 距上一次触发以来的增量字节数(delta),即 loaded - 上次已通报的 loaded |
rate |
number | 可选 | 传输速率(字节/秒),由内部滑动窗口速率表估算;窗口不足时可能为 undefined |
estimated |
number | 可选 | 预计剩余时间(秒),按 (total - loaded) / rate 计算,依赖 rate 和 total 同时存在 |
upload / download |
boolean | 可选 | 方向标记,用于区分同一个回调被用于哪一侧(源码中两者互斥地置 true,见 progressEventReducer.js) |
event |
BrowserProgressEvent | 可选 | 原始底层事件对象(如 XHR 的 progress 事件),仅在浏览器路径下有意义 |
lengthComputable |
boolean | 必填 | 标记 total 是否可计算,等价于 total != null |
注意:文档注释中没有列出
event与lengthComputable两个字段,但它们确实存在于 TS 接口定义与progressEventReducer的产出数据中(progressEventReducer.js),在浏览器端可依赖lengthComputable判断进度条是否"有总量"。
实战:显示上传进度条
结合上表字段,一个典型的上传进度日志实现:
const { data } = await axios.post(SERVER_URL, file, {
onUploadProgress: ({ progress, rate, estimated }) => {
const percent = ((progress ?? 0) * 100).toFixed(1);
const kbps = rate ? (rate / 1024).toFixed(1) : '--';
console.log(`上传中 [${percent}%] 速率 ${kbps} KB/s,预计剩余 ${estimated?.toFixed(1)}s`);
},
});
三、Node.js 流式上传的进度捕获
在 Node.js 中,你还可以将上传与下载进度事件直接对接到一个 readable stream 的场景中捕获。这在需要对流式上传展示自定义进度时非常有用(官方文档给出的示例):
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头。从 lib/adapters/http.js 的源码结构看,http 适配器通过utils.toFiniteNumber(headers.getContentLength())取出请求体的总长,并用它构造progressEventDecorator(contentLength, ...)——装饰器会把total固定为该值,回调中才会出现可计算的progress。没有它,total/progress均为undefined,你只能拿到loaded的单调递增值。 - 建议设置
maxRedirects: 0禁用重定向。官方文档(danger 级别警告)明确说明:Node.js 环境下follow-redirects包在发生重定向时会将整个流缓冲到内存(RAM)中,且不遵循"背压(backpressure)"算法——大文件流式上传遇到 3xx 重定向可能导致内存暴涨。禁用重定向后,3xx 会直接作为响应返回,流可以按背压逐块发送。
FormData 上传进度的限制
文档同时给出一条 warning:目前 FormData 的上传进度捕获在 Node.js 环境中不可用。其根源在于从源码结构看,http 适配器的上传进度依赖将请求体统一转换为 stream 后监听 progress 事件(lib/adapters/http.js),而 Node 端 form-data 包生成的 multipart/form-data 边界流没有可靠的总长度信号,total 无法确定,因此该能力尚未支持。若需要大表单进度,可考虑改用二进制流 + Content-Length 的方式。
四、源码深潜:进度事件是如何产生并节流的
三个适配器共用同一套进度事件工具链,核心在 lib/helpers/progressEventReducer.js、lib/helpers/throttle.js 与 lib/helpers/speedometer.js。
4.1 节流器:leading + trailing 边沿的每秒 3 次
throttle.js 实现的是一个带尾随调用的节流装饰器:
- 时间阈值
threshold = 1000 / freq,freq = 3时约为 333ms; - 若两次调用间隔
>= threshold,立即同步触发(leading 边沿); - 否则缓存最后一次参数
lastArgs,并在threshold - passed毫秒后通过setTimeout补发一次(trailing 边沿); - 返回
[throttled, flush]元组,flush用于在流结束前把缓存的最后一次事件强制刷出。
这就是为什么进度条的最后一帧总能到达 100%——即使最后一次网络事件落在节流窗口内,也会在结束时被 flush 出来。
4.2 速率表:50 个样本的滑动窗口
rate 字段(字节/秒)来自 speedometer.js:它维护一个最多 50 个样本的环形缓冲,每个样本记录一次 bytes 增量及其时间戳。关键行为:
- 首次调用到当前时间不足
min(progress 场景传入 250ms,见 progressEventReducer.js)时返回undefined,所以请求最初约 1/4 秒内rate/estimated可能为空; - 达到窗口后,速率 = 窗口内总字节数 / 窗口经过时间(毫秒),即
Math.round((bytesCount * 1000) / passed)。
estimated(预计剩余秒数)则是在 progressEventReducer.js 中由 rate && total ? (total - loaded) / rate : undefined 推导的。
4.3 浏览器路径:XHR 的 progress 事件
在 lib/adapters/xhr.js 中,适配器为 XHR 对象挂接节流后的监听器:
// 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);
}
两个值得注意的实现细节:
request.upload兼容性检查:并非所有浏览器都支持上传进度事件,源码会先判断request.upload存在才注册,不支持的浏览器中onUploadProgress静默不触发;loadend时 flush:上传结束时强制把节流窗口内缓存的最后一个事件刷出,保证loaded === total的终态事件一定送达。
4.4 Node.js 路径:transform stream + 异步调度
http 适配器(lib/adapters/http.js)的上传路径是:请求体(无论 Buffer 还是 Stream)被包装进 AxiosTransformStream 管道,该流在每次 write 后发出 progress 事件;回调再经过三层装饰:
progressEventDecorator( // 注入固定的 total(来自 Content-Length)
contentLength,
progressEventReducer( // 节流 3 次/秒 + 计算 rate/estimated
asyncDecorator(onUploadProgress, scheduleProgress), // 回调放到微任务队列执行,避免阻塞 IO
false,
3
)
)
asyncDecorator(progressEventReducer.js)把用户回调放入utils.asap调度,避免在 stream 的写路径中同步执行业务代码而阻塞背压;flushOnFinish监听流结束,把最后一次未触发的事件刷出,与浏览器端的loadendflush 逻辑对齐。
下载路径(lib/adapters/http.js)完全同构:响应流 res 串接一个 AxiosTransformStream,total 取自响应头的 content-length,进度监听器同样按 freq = 3 节流。fetch 适配器(lib/adapters/fetch.js 与 lib/adapters/fetch.js)也复用同一套 progressEventDecorator / progressEventReducer,保证三种适配器下事件字段语义一致。
补充:Node.js 中该管道还与带宽限制能力复用——当同时配置
maxRate(上传/下载限速)时,AxiosTransformStream承担限速职责,进度事件照常发出。maxRate的用法详见 docs/pages/advanced/rate-limiting.md,它仅在 Node.js HTTP 适配器下生效,浏览器中无效。
4.5 测试佐证
仓库中的冒烟测试 tests/smoke/esm/tests/progress.smoke.test.js 验证了上述行为:
- 上传用例:以
Readable.from(payload)作为请求体并显式设置Content-Length,断言收到的最后一个事件满足{ loaded: total, total, upload: true }——验证了"终态 flush"与方向标记; - 下载用例:通过自定义 transport 响应头携带
content-length,断言最后一个事件为{ loaded: total, total, download: true },且response.data内容完整。
浏览器端的对应测试见 tests/browser/progress.browser.test.js,http/fetch 适配器的单测见 tests/unit/adapters/http.test.js 与 tests/unit/adapters/fetch.test.js,可进一步查看各环境下的断言细节。
五、要点回顾
onUploadProgress/onDownloadProgress在浏览器与 Node.js 双端可用,事件频率被硬编码节流为每秒 3 次(leading + trailing 边沿),且结束时保证 flush 出 100% 终态事件;total/progress是否有值取决于环境能否给出总量:浏览器看lengthComputable,Node.js 看Content-Length(上传)/content-length(响应头);rate与estimated基于 250ms 起步的 50 样本滑动窗口估算,请求初期可能为undefined,UI 层需要兜底;- Node.js 流式上传务必显式设置
Content-Length,并建议maxRedirects: 0以避免follow-redirects将整个流缓冲进内存; - FormData 上传进度在 Node.js 环境暂不支持,浏览器中 XHR
upload事件缺失时上传回调静默不触发。
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