使用Fetch API实现JavaScript大文件下载进度监控

作者:半夏

时间:2026-10-30

浏览:618

本文详细讲解如何使用现代Fetch API配合ReadableStream实现大文件下载的实时进度监控,包含核心原理、完整代码示例、CORS配置与异常边界处理。

文章目录

在日常 Web 业务场景中,当用户需要导出几十兆到数吉字节的分析报表、备份归档包或多媒体资源时,前端通常需要给出一个明确的进度指示。如果单纯使用 window.location.href 或 <a download> 标签,浏览器会交出请求控制权,前端脚本完全无法感知下载传输的实时状态;而如果直接使用常规的 fetch(url).then(res => res.blob()),Promise 会在所有二进制字节全部接收完毕后才一次性 resolve,在长达数秒乃至数分钟的数据传输过程中,用户界面处于完全无反馈的“假死”状态。

要解决这一问题,核心路径在于利用 Fetch API 响应体所携带的 Streams API(即 ReadableStream),逐块(chunk)拉取底层二进制流并持续累计已接收字节,结合响应标头中的 Content-Length 即可精确换算出下载百分比,最后将数据块组装为文件触发本地落盘。

一、Fetch API 流式读取的核心机制

传统的 XMLHttpRequest 虽然提供了 xhr.onprogress 事件,但其 API 基于老旧的事件监听机制,且在现代复杂异步流控制(如响应式管道、中间件链式处理)中显得笨重。现代标准的 Fetch API 拥抱了流式思想:当执行 const response = await fetch(url) 时,返回的 response.body 实际上是一个 ReadableStream 实例。

调用 response.body.getReader() 可以获取一个读取锁定的 reader(ReadableStreamDefaultReader)。通过循环执行 await reader.read(),网络层会将每一次从 TCP 缓冲区接收到的数据分段包装为一个 Uint8Array 交付给 JavaScript 运行时。我们在读取每个 chunk 的同时累加其 length,就能掌握第一手、毫无延迟的传输进度。

使用Fetch API实现JavaScript大文件下载进度监控
图 1:传统全量阻塞加载与 Streams API 流式切片管道读取机制对比

二、核心实施步骤与代码详解

实现进度监控由四个严格按序执行的关键环节构成:检查响应头与初始化统计指标、创建流读取器、执行异步循环消费数据块、组装二进制产物并触发本地下载。

步骤 1:响应校验与提取总字节数

通过 Fetch 发送 GET 请求后,首先确认 HTTP 状态码为 200 级别,随后从 response.headers 获取 content-length。需要注意该值是一个十进制字符串,必须通过 parseInt(..., 10) 转换为数值。

const response = await fetch(fileUrl);
if (!response.ok) {
  throw new Error(`下载失败,HTTP状态码: ${response.status}`);
}

// 从标头提取 Content-Length(总字节数)
const contentLength = response.headers.get('content-length');
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;

步骤 2:获取 Stream Reader 并迭代接收 Chunk

通过 response.body.getReader() 拿到读取器。使用 while (true) 循环调用 reader.read(),直到返回的 done 属性为 true。每次读取到的 value 即为类型化数组 Uint8Array,我们将其暂存在一个数组中,并累加已接收长度。

const reader = response.body.getReader();
let receivedBytes = 0;
const chunks = [];

while (true) {
  const { done, value } = await reader.read();
  if (done) {
    break;
  }
  
  chunks.push(value);
  receivedBytes += value.length;
  
  // 计算进度百分比并执行回调
  if (totalBytes > 0) {
    const percent = ((receivedBytes / totalBytes) * 100).toFixed(2);
    onProgress({ percent, receivedBytes, totalBytes });
  }
}

步骤 3:数据块合并与文件触发

循环结束后,将缓存的多个 Uint8Array 数据块封装进一个 Blob 对象中。通过 URL.createObjectURL(blob) 生成临时内存引用,借助动态创建的 <a download> 标签模拟点击,最后及时调用 URL.revokeObjectURL 释放内存。

使用Fetch API实现JavaScript大文件下载进度监控
图 2:Fetch 大文件下载进度监控端到端执行步骤

三、关键细节与边界情况处理

在实际生产环境中,大文件下载经常会遭遇复杂的网络策略与边界状态。如果不针对这些边界设计容错,程序很容易出现无进度指示、内存溢出甚至无法取消的问题。

1. CORS 跨域标头暴露问题

默认情况下,浏览器的跨域安全模型仅允许前端脚本访问“简单响应头”(如 Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma)。而用于计算进度的 Content-Length 属于非简单响应头,在跨域请求时被浏览器默认隐藏!

服务端必须配置的响应头

后端服务端或反向代理(Nginx/CDN)必须在响应头中显式添加:Access-Control-Expose-Headers: Content-Length,否则前端 response.headers.get('content-length') 将返回 null。

2. 分块传输编码(Transfer-Encoding: chunked)

如果服务端采用动态压缩或流式生成(例如实时打包 ZIP 或大文件服务端动态加密输出),响应头通常不包含 Content-Length,而是采用 Transfer-Encoding: chunked。此时前端无法预知确切总字节数。健壮的实现应当支持降级方案:若 totalBytes === 0,进度条不展示具体百分比,转为显示“已下载 XX MB(加载中)”的不确定动画。

3. 下载中断与资源清理

大文件耗时长,必须提供“取消下载”按钮。通过传入 AbortController.signal,当用户点击取消时触发 controller.abort(),同时在 catch 块中调用 reader.cancel(),立即切断底层 TCP 连接,避免带宽浪费与内存持续泄漏。

使用Fetch API实现JavaScript大文件下载进度监控
图 3:跨域标头暴露、Chunked 降级与中断清理三维边界控制

四、工程级完整通用封装

下面是一个工业级可用的通用下载函数 downloadFileWithProgress,集成了进度回调、速率测算、文件名自动推导及主动取消功能,可以直接复制并在项目中使用:

/**
 * 带有进度监控的 Fetch 大文件下载工具函数
 * @param {string} url 下载地址
 * @param {Object} options 配置项
 * @param {string} [options.filename] 保存文件名(可选,默认从标头或URL解析)
 * @param {Function} [options.onProgress] 进度回调函数 ({ percent, loaded, total, speed })
 * @param {AbortSignal} [options.signal] 取消信号
 * @returns {Promise<Blob>}
 */
async function downloadFileWithProgress(url, options = {}) {
  const { filename, onProgress, signal } = options;
  
  const response = await fetch(url, { signal });
  if (!response.ok) {
    throw new Error(`网络请求异常: ${response.status} ${response.statusText}`);
  }

  const contentLength = response.headers.get('content-length');
  const total = contentLength ? parseInt(contentLength, 10) : 0;
  
  const reader = response.body.getReader();
  const chunks = [];
  let loaded = 0;
  let startTime = performance.now();
  let lastTime = startTime;
  let lastLoaded = 0;
  let speed = 0; // 字节/秒

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      chunks.push(value);
      loaded += value.length;

      // 测算实时下载速度(按秒平滑采样)
      const now = performance.now();
      const elapsed = (now - lastTime) / 1000;
      if (elapsed >= 0.3) {
        speed = (loaded - lastLoaded) / elapsed;
        lastTime = now;
        lastLoaded = loaded;
      }

      if (onProgress) {
        const percent = total > 0 ? parseFloat(((loaded / total) * 100).toFixed(2)) : 0;
        onProgress({ percent, loaded, total, speed });
      }
    }
  } catch (err) {
    await reader.cancel();
    throw err;
  }

  // 聚合所有数据段并触发文件落盘
  const blob = new Blob(chunks);
  const downloadUrl = URL.createObjectURL(blob);
  const anchor = document.createElement('a');
  anchor.href = downloadUrl;
  anchor.download = filename || url.split('/').pop() || 'downloaded_file';
  document.body.appendChild(anchor);
  anchor.click();
  document.body.removeChild(anchor);
  URL.revokeObjectURL(downloadUrl);

  return blob;
}

五、全景架构与选型 Checklist 总结

使用 Fetch API 配合 Streams API 是当今现代前端实现数据加载进度感知最标准、无侵入且性能卓越的解决方案。在工程落地上,建议按照以下图解的技术要点清单进行逐一审查:

使用Fetch API实现JavaScript大文件下载进度监控
图 4:Fetch 大文件流式下载全景四维知识体系与工程验收 Checklist

综上所述,借助 Fetch API 的 response.body 与 ReadableStreamDefaultReader,前端不仅可以摆脱老旧 XMLHttpRequest 的代码包袱,更能实现颗粒度精确到单个 TCP 分块的实时下载进度监控与速度测算。在结合正确的服务端 CORS 响应标头与 AbortController 取消控制后,即可为用户构建稳定、透明且体验出色的现代大文件下载能力。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章