首页
/ Axios 进度捕获完全指南:onUploadProgress / onDownloadProgress 原理与实战

Axios 进度捕获完全指南:onUploadProgress / onDownloadProgress 原理与实战

2026-09-06 13:18:07作者:邓越浪Henry

本文基于 axios 官方文档 进度捕获 编写,系统讲解如何在浏览器与 Node.js 环境中捕获 HTTP 请求的上传/下载进度:包括 AxiosProgressEvent 各字段的确切含义、进度事件被限流到每秒 3 次的实现机制、Node.js 下流式上传进度捕获的正确姿势,以及 follow-redirects 缓冲内存陷阱的规避方法。读完后你将能独立实现进度条、ETA 估算与速度显示,并理解进度事件从 xhr/http/fetch 三个适配器汇聚到同一回调的底层链路。

一、为什么需要进度捕获,以及 axios 给出的约束

上传大文件、下载大资源时,用户界面通常需要展示“已传 X%、剩余 Y 秒、当前速度 Z KB/s”这类信息。axios 在浏览器(xhr/fetch 适配器)和 Node.js(http 适配器)环境下同时支持捕获请求的上传与下载进度,两个入口是请求配置中的两个回调:

  • onUploadProgress:上传进度回调
  • onDownloadProgress:下载进度回调

文档明确给出一个关键约束:进度事件的触发频率被强制限制为每秒最多 3 次,目的是避免浏览器被过高的进度事件频率压垮(例如上传一个 1GB 文件时,浏览器原生的 progress 事件可能每秒触发数百次)。这个限制不是可配置项,而是写死在源码 lib/helpers/progressEventReducer.js 中的默认参数 freq = 3

二、AxiosProgressEvent 字段全解

文档给出的捕获示例如下,两个回调接收同一个 AxiosProgressEvent 对象,通过 upload/download 标识区分方向:

await axios.post(url, data, {
  onUploadProgress: function (axiosProgressEvent) {
    /*{
      loaded: number;
      total?: number;
      progress?: number; // 范围 [0..1]
      bytes: number; // 自上次触发以来传输的字节数(增量)
      estimated?: number; // 预计剩余时间(秒)
      rate?: number; // 上传速度(字节/秒)
      upload: true; // 上传标识
    }*/
  },

  onDownloadProgress: function (axiosProgressEvent) {
    /*{
      loaded: number;
      total?: number;
      progress?: number;
      bytes: number;
      estimated?: number;
      rate?: number; // 下载速度(字节/秒)
      download: true; // 下载标识
    }*/
  },
});

结合 TypeScript 声明 index.d.ts 中的 AxiosProgressEvent 接口,可以给出比文档注释更完整的字段表:

字段 类型 说明
loaded number 已传输的字节数(绝对值,单调不减)
total number? 总字节数;仅当对端/传输层可计算总长时才有值
progress number? loaded / total,范围 [0..1]total 未知时为 undefined
bytes number 自上次触发以来新增的字节数(增量,非累计)
rate number? 传输速度,字节/秒;采样窗口不足时可能为 undefined
estimated number? 预计剩余时间(秒),由 (total - loaded) / rate 推出
upload / download boolean? 方向标识,二者互斥
event any? 底层原始事件(如浏览器 ProgressEvent),源码中额外附带
lengthComputable boolean? 是否可计算总长度,源码中额外附带

几个容易踩坑的细节,均可在 progressEventReducer.js 中得到印证:

  1. progresstotal 是可选的。服务端未返回 Content-Length(如分块传输、chunked 编码的流式响应)时,totalprogress 都是 undefined,此时只能依赖 loadedbytes 做纯增量展示。
  2. loaded 做了防回退与截断保护:源码中 loaded = Math.max(0, total != null ? Math.min(rawLoaded, total) : rawLoaded),即不会出现 loaded > total 或负值。
  3. rateestimated 是推导值:速度由滑动窗口采样器计算(下文详述),前 250ms 内样本不足时 rateundefinedestimated 则要求 ratetotal 同时存在。
  4. event 字段在文档注释中未提及,但 index.d.ts 声明了 event?: BrowserProgressEvent,源码在 progressEventReducer.js 中将其原样透传,高级场景可据此读取底层事件。

三、每秒 3 次限流是怎么实现的

“每秒最多 3 次”这一约束由两个工具函数协作实现,这是理解整条进度链路的核心。

3.1 throttle:节流 + 尾冲刷(trailing flush)

lib/helpers/throttle.js 实现的不是一个“丢事件”的简单节流器:

function throttle(fn, freq) {
  let timestamp = 0;
  let threshold = 1000 / freq; // freq=3 时约 333ms
  let lastArgs;
  let timer;

  const invoke = (args, now = Date.now()) => { /* 立即触发一次 fn(...) */ };

  const throttled = (...args) => {
    const now = Date.now();
    const passed = now - timestamp;
    if (passed >= threshold) {
      invoke(args, now);            // 距上次触发超过阈值:立即触发
    } else {
      lastArgs = args;               // 否则记录最后一次参数
      if (!timer) {
        timer = setTimeout(() => {
          timer = null;
          invoke(lastArgs);         // 窗口结束时补发最后一次
        }, threshold - passed);
      }
    }
  };

  const flush = () => lastArgs && invoke(lastArgs);

  return [throttled, flush];        // 注意:返回 [节流函数, 冲刷函数] 元组
}

两个设计要点:

  • 返回的是 [throttled, flush] 元组而非单个函数。这是为了在传输结束时把被节流缓存的最后一次事件补发出去,保证进度条能精确停在 100%,而不是停在 98%。
  • 尾冲刷机制(trailing call)保证即使两次触发间隔不足 333ms,窗口结束时也会用最后一次参数触发,因此进度值不会“丢帧”。

3.2 speedometer:滑动窗口测速

rate(速度)字段的来源是 lib/helpers/speedometer.jsprogressEventReducer.js 中以 speedometer(50, 250) 构造实例,即环形缓冲区最多保留 50 个采样、最短采样窗口 250ms:

export const progressEventReducer = (listener, isDownloadStream, freq = 3) => {
  let bytesNotified = 0;
  const _speedometer = speedometer(50, 250);

  return throttle((e) => {
    if (!e || typeof e.loaded !== 'number') {
      return;
    }
    const rawLoaded = e.loaded;
    const total = e.lengthComputable ? e.total : undefined;
    const loaded = Math.max(0, total != null ? Math.min(rawLoaded, total) : rawLoaded);
    const progressBytes = Math.max(0, loaded - bytesNotified); // 增量
    const rate = _speedometer(progressBytes);                  // 滑窗测速

    bytesNotified = Math.max(bytesNotified, loaded);

    const data = {
      loaded,
      total,
      progress: total ? loaded / total : undefined,
      bytes: progressBytes,
      rate: rate ? rate : undefined,
      estimated: rate && total ? (total - loaded) / rate : undefined,
      event: e,
      lengthComputable: total != null,
      [isDownloadStream ? 'download' : 'upload']: true,
    };

    listener(data);
  }, freq);
};

progressEventReducer 将任意来源的“原始进度”归一化为统一的 AxiosProgressEvent 并交给用户回调;isDownloadStream 参数决定打上 download: true 还是 upload: true 标识——这解释了为什么同一个对象类型能同时服务上传与下载回调。

四、三个适配器中的进度链路

4.1 浏览器 XHR 适配器

lib/adapters/xhr.js 中,进度事件直接来自原生 XMLHttpRequestprogress 事件:

// 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 并非所有浏览器都有(注释 “Not all browsers support upload events”),没有 upload 对象时上传进度回调会被静默跳过。另外 loadend 上挂的 flushUpload 正是 3.1 节提到的尾冲刷,用来保证上传结束时补发最后一次进度。

4.2 Node.js HTTP 适配器

Node 端的原生 HTTP 流没有 progress 事件,axios 的做法是把请求体/响应流串入一个自定义 Transform 流来统计字节数lib/adapters/http.js 中上传侧的接线如下:

if (data && (onUploadProgress || maxUploadRate)) {
  if (!utils.isStream(data)) {
    data = stream.Readable.from(data, { objectMode: false });
  }

  data = stream.pipeline(
    [
      data,
      new AxiosTransformStream({
        maxRate: utils.toFiniteNumber(maxUploadRate),
      }),
    ],
    utils.noop
  );

  onUploadProgress &&
    data.on(
      'progress',
      flushOnFinish(
        data,
        progressEventDecorator(
          contentLength,
          progressEventReducer(asyncDecorator(onUploadProgress, scheduleProgress), false, 3)
        )
      )
    );
}

其中 progressEventDecorator(total, throttled)(见 progressEventReducer.js)负责把“已读字节数”包装成 { lengthComputable, total, loaded } 形态,total 即请求头里的 Content-Length。这也意味着:Node 上传进度能显示 total/progress 的前提是设置了 Content-Length——这正是文档示例中显式带上 headers: { "Content-Length": contentLength } 的原因。

[lib/helpers/AxiosTransformStream.js](https://gitcode.com/GitHub_Trending/ax/axios/blob/2d2a21af8a433089474a2149781799c93acbcf3c/lib/helpers/AxiosTransformStream.js?utm_source=gitcode_repo_files#L8-L50) 是这条链路的心脏:

class AxiosTransformStream extends stream.Transform {
  constructor(options) {
    options = utils.toFlatObject(
      options,
      {
        maxRate: 0,
        chunkSize: 64 * 1024,
        minChunkSize: 100,
        timeWindow: 500,
        ticksRate: 2,
        samplesCount: 15,
      },
      null,
      /* ... */
    );
    /* ... */
  }
  /* _transform 中每推入一个 chunk 就累计 bytesSeen,
     若有人监听了 'progress' 事件则 this.emit('progress', internals.bytesSeen) */
}

它按 chunkSize(默认 64KB)切分数据块,每推入一块就累计 bytesSeen 并 emit progress 事件;当配置了 maxUploadRate/maxDownloadRate 时,它还顺带完成按时间窗的限速。下载侧在 http.js 中以同样方式把 AxiosTransformStream 插入响应流管道,total 取自响应头 content-length

4.3 Fetch 适配器

lib/adapters/fetch.js 中,当请求体是流时,axios 通过 lib/helpers/trackStream.js 把流重新包装成一个 ReadableStream,在每次 pull 读到一个 chunk 后累计字节并回调 onProgress

if (_request.body) {
  const [onProgress, flush] =
    (onUploadProgress &&
      progressEventDecorator(
        requestContentLength,
        progressEventReducer(asyncDecorator(onUploadProgress))
      )) ||
    [];

  data = trackRequestStream(_request.body, onProgress, flush);
}

下载侧(fetch.js)同样走 progressEventDecorator + progressEventReducer。从源码结构看,三个适配器最终都汇聚到同一对 progressEventReducer / progressEventDecorator 工具函数,这是 AxiosProgressEvent 在各环境中字段语义完全一致的根本原因。

五、Node.js 下流式上传进度的实战写法

文档给出的第二个示例面向一个高频场景:请求体本身是一个可读流(例如从磁盘逐块读取大文件上传)。此时不能先读完整体,而要靠流式进度事件实时渲染:

const { data } = await axios.post(SERVER_URL, readableStream, {
  onUploadProgress: ({ progress }) => {
    console.log((progress * 100).toFixed(2));
  },

  headers: {
    "Content-Length": contentLength,
  },

  maxRedirects: 0, // 避免缓冲整个流
});

这段代码中每个配置项都有明确的底层依据,不可随意删减:

配置项 作用 底层依据
headers: { "Content-Length": contentLength } 让进度事件携带 total,从而 progress 有值 Node 侧 total 取自请求头 Content-Lengthhttp.jsutils.toFiniteNumber(headers.getContentLength())),缺失时 progressundefined
maxRedirects: 0 避免整个流被缓冲进内存 见下文 danger 说明
流式 readableStream 作为 data 内存占用恒定 http 适配器会把任意 data 转为 Readable 串入管道(http.js

5.1 为什么必须 maxRedirects: 0

文档中的 danger 提示值得逐字重视:Node.js 环境中上传流时建议通过 maxRedirects: 0 禁用重定向,因为 follow-redirects 包在不遵循“背压(backpressure)”算法的情况下会把整个流缓冲到内存(RAM)中

也就是说,如果你上传一个 10GB 的流且目标 URL 发生 30x 重定向,follow-redirects 为了在重定向后重放请求体会把整个 10GB 读到内存里——这与流式上传“恒定内存”的初衷完全相悖。maxRedirects: 0 让重定向在第一次跳转时直接失败(返回 30x 状态码),由应用层决定如何处理,从而保住流的背压语义。

5.2 已知限制:Node.js 不支持 FormData 上传进度

文档明确警告:Node.js 环境目前不支持捕获 FormData 上传进度。从源码结构看,这与 Node 端 FormData 会被序列化为流且难以预知边界长度有关,上传进度链路依赖 Content-Length 与可计数字节流,multipart 流难以满足这一前提。如果业务必须同时满足“multipart 上传 + 进度条”,从源码结构看可以考虑改用浏览器端 fetch 适配器或自行按固定块上传(如 tus 类分片协议),但不宜指望 onUploadProgress 在 Node + FormData 组合下产生有效事件。

六、完整实战:进度条 + 速度 + ETA

把前面各节拼起来,一个可直接运行的上传进度实现如下(Node.js,请求体为 Readable 流):

import axios from 'axios';
import fs from 'node:fs';

const file = fs.createReadStream('./large-file.bin');
const contentLength = fs.statSync('./large-file.bin').size;

await axios.post('https://example.com/upload', file, {
  headers: { 'Content-Length': contentLength },
  maxRedirects: 0, // 保留背压,避免 follow-redirects 全量缓冲
  onUploadProgress: ({ loaded, total, progress, rate, estimated }) => {
    const pct = progress !== undefined ? (progress * 100).toFixed(1) : '??';
    const speed = rate ? (rate / 1024).toFixed(1) + ' KB/s' : '--';
    const eta = estimated ? Math.ceil(estimated) + 's' : '--';
    console.log(`[${pct}%] ${loaded} / ${total ?? '?'} bytes | ${speed} | ETA ${eta}`);
  },
});

浏览器端则无需 Content-LengthmaxRedirects 配置,直接监听 XHR 的 progress 事件即可,写法与文档第一个示例一致;total 是否可算由服务端是否返回 Content-Length 决定,前端代码应对 progress === undefined 做兜底。

七、测试佐证与延伸阅读

进度功能在仓库中有覆盖多个环境的测试集,可作为行为验证依据:

核心文件索引,便于继续深挖:

小结:axios 的进度捕获对外暴露为 onUploadProgress/onDownloadProgress 两个回调和统一的 AxiosProgressEvent 结构,对内则由 progressEventReducer(字段归一化)、throttle(3 次/秒限流 + 尾冲刷)、speedometer(滑窗测速)三件套支撑,并借助 AxiosTransformStream/trackStream 在 Node 与 Fetch 的流场景下统计字节数。工程实践上有三条硬约束需要牢记:进度事件固定约 3 次/秒、total/progress 依赖 Content-Length、Node 端流式上传必须 maxRedirects: 0 且暂不支持 FormData 上传进度。

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