首页
/ Axios 进度捕获实战:onUploadProgress 与 onDownloadProgress 在浏览器和 Node.js 中的实现原理

Axios 进度捕获实战:onUploadProgress 与 onDownloadProgress 在浏览器和 Node.js 中的实现原理

2026-09-06 10:59:30作者:曹令琨Iris

本文基于 axios 官方文档 Progress capturing 展开,讲解如何在浏览器与 Node.js 两种环境中捕获上传/下载进度事件、理解 AxiosProgressEvent 各字段的含义与默认值,并结合 进度事件归约器节流工具xhrhttpfetch 三个适配器的源码,深入剖析"每秒 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) / rateratetotal 缺失时无此字段
upload boolean 上传标记。仅上传进度事件携带 upload: true
download boolean 下载标记。仅下载进度事件携带 download: true
event any 原始底层事件对象(浏览器为原生 ProgressEvent,Node 下为装饰后的事件),类型定义中记为 BrowserProgressEvent
lengthComputable boolean 本次事件能否计算总长,即 total != null 的布尔结果

可以看到,官方文档注释里的字段是核心子集,而类型定义与源码还额外暴露了 eventlengthComputable,方便在回调里判断"是否处于可计算进度状态"。

核心机制之一:强制 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);
};

三个关键点:

  1. 默认频率 freq = 3,且三个适配器(xhr/http/fetch)在 Node 侧均以字面量 3 显式传入,浏览器侧使用默认值。也就是说,每秒 3 次是硬性约定,不暴露配置项。
  2. 节流不是简单丢弃throttle.js 返回一个二元组 [throttled, flush]throttled 在时间阈值(1000 / freq 毫秒)内合并调用、只保留最新参数,并挂一个 setTimeout 延迟补发;flush 则立即把最后一次被合并的参数"冲刷"出去。适配器在上传流结束时调用 flush(见下文),保证进度条最终停在 100%,而不是停在最后一次节流时刻。
  3. 增量 bytesbytesNotified 维护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.uploadUpload 对象),源码用 request.upload 的存在性做了特性探测——"Not all browsers support upload events";
  • loadend 时执行 flushUpload,把最后一次被节流合并的上传事件强制补发,保证收尾事件不丢失。

核心机制之二:速率与剩余时间估算(speedometer)

rateestimated 字段由 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 组合得出。这也解释了为什么文档中 rateestimated 均为可选字段:冷启动阶段或 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
});

这段示例有三个实操要点,全部与源码对应:

  1. 手动声明 Content-Length 才能算出 progressprogress = 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)
          )
        )
      );
    

    不声明该头时 totalundefined,回调里只有 loadedbytes 可用,progressundefined

  2. 上传数据被包装为带 progress 事件的流管道。Node 侧并非直接"监听字节数",而是把请求体经过 stream.pipeline([data, new AxiosTransformStream({ maxRate })])http.js L859-L867),由转换流按块发出 progress 事件,再经 asyncDecorator(onUploadProgress, scheduleProgress) 调度到异步回调中执行,避免在流事件循环中阻塞事件循环。

  3. 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 的进度来自响应流上串联的 AxiosTransformStreamhttp.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 / maxDownloadRatetype 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 等多处)逐一断言了 loadedtotalprogressbytesrateupload/download 各字段的实际值;tests/unit/adapters/fetch.test.js 验证 fetch 适配器,tests/smoke/esm/tests/progress.smoke.test.js 等 smoke 测试覆盖跨环境的基本链路。

小结

  • 进度捕获通过请求配置中的 onUploadProgress / onDownloadProgress 接入,事件对象在三种适配器(xhr / http / fetch)中语义一致;
  • 事件频率被 progressEventReducer 强制节流到 3 次/秒,并用 throttleflush 机制保证收尾事件不丢失;
  • progressrateestimated 均为可选字段,取决于 Content-Length 是否存在与采样窗口是否达到 250ms;
  • Node.js 流式上传建议显式声明 Content-Length 并设置 maxRedirects: 0,避免 follow-redirects 整流缓冲;Node 环境下 FormData 上传暂不支持进度捕获。

以上机制均可在 lib/helpers/progressEventReducer.jslib/helpers/throttle.jslib/helpers/speedometer.js 与三个适配器源码中直接查证。

登录后查看全文
热门项目推荐
相关项目推荐