当前位置:首页 > 文章列表 > 文章 > 前端 > Fetch API流式读取响应并显示下载进度的实现

Fetch API流式读取响应并显示下载进度的实现

来源:17golang原创 2026-09-20 08:48:35 0浏览 收藏

下载大文件时,如果直接调用 response.blob()response.arrayBuffer(),业务代码通常要等整个响应读完才拿到结果。需要实时显示进度时,应改为读取 response.body:它是一个 ReadableStream,每次得到一块 Uint8Array,累计字节数后再更新界面。

真正可靠的实现有两个前提:响应头能提供 Content-Length 时才计算百分比;没有总长度时只展示“已接收多少”,不要把未知状态硬算成 0% 或 100%。下面的方案还会补上取消请求、异常提示和 Blob URL 清理。

要点速览
  • response.body.getReader() 让下载响应按块到达,value.length 是当前字节块的大小。
  • Content-Length 缺失并不表示下载失败,只表示无法可靠计算整体百分比。
  • 文件合并为 Blob 后要释放 URL.createObjectURL() 创建的临时 URL。

先把 Fetch 的响应流接到可观察状态

示例准备一个按钮、进度条和状态文本。函数不假定框架,调用方只需要传入下载地址和文件名。先检查 HTTP 状态与 body,再读取响应头;这样 404 或没有响应体时不会在后面的 getReader() 位置才暴露模糊异常。

// 中文注释:这些元素分别承载取消按钮、进度条和可读状态
const downloadButton = document.querySelector('#downloadButton');
const cancelButton = document.querySelector('#cancelButton');
const progressBar = document.querySelector('#progressBar');
const statusText = document.querySelector('#statusText');

let activeController = null;

function formatBytes(bytes) {
  // 中文注释:用二进制单位显示已接收大小,避免界面只出现很长的数字
  if (bytes 

这里的 activeController 只保存当前请求。新的下载开始前先阻止重复点击,取消按钮则调用同一个控制器;不要为每个 UI 事件临时创建多个互相不知道的取消对象。

Fetch Response body ReadableStream reader read 和已接收字节数之间的静态关系图
图1:Fetch 响应体从 Response.body 进入 ReadableStream,再由 reader.read() 返回字节块并累加接收量。

用 reader.read() 累计每个响应块

核心循环只关心两个返回值:done 表示流已经结束,value 是本次到达的字节数组。读取器会锁定流,因此不要在同一个 response.body 上再调用 text()blob();两套消费方式只能选一套。

// 中文注释:流式下载并返回 Blob;signal 让调用方可以主动取消
async function downloadWithProgress(url, filename, signal) {
  const response = await fetch(url, { signal });
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  if (!response.body) {
    throw new Error('响应没有可读取的 body');
  }

  // 中文注释:Content-Length 不存在时 totalBytes 为 null,进度必须保持不确定状态
  const lengthHeader = response.headers.get('Content-Length');
  const totalBytes = lengthHeader ? Number(lengthHeader) : null;
  const reader = response.body.getReader();
  const chunks = [];
  let receivedBytes = 0;

  while (true) {
    // 中文注释:每次 read 等待下一块数据,直到 done=true 才结束循环
    const { done, value } = await reader.read();
    if (done) break;
    if (!value) continue;

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

    if (Number.isFinite(totalBytes) && totalBytes > 0) {
      const percent = Math.min(100, (receivedBytes / totalBytes) * 100);
      progressBar.max = 100;
      progressBar.value = percent;
      progressBar.removeAttribute('aria-busy');
      statusText.textContent = `${percent.toFixed(1)}%(${formatBytes(receivedBytes)} / ${formatBytes(totalBytes)})`;
    } else {
      // 中文注释:没有可靠总长度时只报告已接收字节,不伪造百分比
      progressBar.removeAttribute('value');
      progressBar.setAttribute('aria-busy', 'true');
      statusText.textContent = `已接收 ${formatBytes(receivedBytes)},总大小未知`;
    }
  }

  return new Blob(chunks, {
    type: response.headers.get('Content-Type') || 'application/octet-stream'
  });
}

Content-Length 的单位是字节,但它可能不存在,或者因为动态生成、分块传输、代理处理而不能代表最终可显示的总量。因此代码把它解析为有限正数后才计算百分比。即便收到的字节数超过了头部值,也只把界面上限夹到 100%,不要据此断言文件内容一定完整。

把两种进度语义明确地呈现出来

有总长度时,进度条可以表达比例;没有总长度时,进度条只能表达“仍在读取”。如果页面必须显示百分比,应让服务端提供稳定的长度,或改用分片协议单独传递总大小。CORS 场景还要确认响应头能够被浏览器脚本读取,否则 headers.get('Content-Length') 仍可能得到 null

响应条件界面状态实现建议
Content-Length 是正数确定进度按 receivedBytes / totalBytes 更新百分比
没有 Content-Length不确定进度显示已接收大小和“总大小未知”
响应失败或 body 为空失败状态停止进度并给出 HTTP 状态或读取错误
Fetch下载进度中Content-Length已知未知分支以及AbortController和Blob URL资源边界图
图2:Content-Length 决定是否能计算百分比;AbortController、Blob 和 object URL 则负责请求取消与下载资源清理。

完成下载后触发文件保存并释放 URL

拿到 Blob 后创建临时 URL,借助隐藏的 a 元素触发浏览器保存。点击完成后仍要调用 URL.revokeObjectURL();它不是可选的“性能优化”,而是避免临时对象长期占用内存的收尾动作。

// 中文注释:把 Blob 转成一次性下载链接,并在点击后释放临时 URL
function saveBlob(blob, filename) {
  const objectUrl = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = objectUrl;
  link.download = filename;
  link.click();

  // 中文注释:延迟释放,给浏览器完成本次点击导航的机会
  setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
}

把取消、失败和按钮复位放进同一条路径

完整调用还要区分用户主动取消和真正失败。AbortController 抛出的通常是 AbortError,它不应该被页面提示成服务器故障;而 finally 负责无论成功还是失败都恢复按钮状态。

// 中文注释:统一管理一次下载的开始、取消、成功和失败状态
async function startDownload() {
  if (activeController) return;

  activeController = new AbortController();
  downloadButton.disabled = true;
  cancelButton.disabled = false;
  statusText.textContent = '正在连接…';

  try {
    const blob = await downloadWithProgress(
      '/files/report.zip',
      'report.zip',
      activeController.signal
    );
    saveBlob(blob, 'report.zip');
    statusText.textContent = '下载完成';
  } catch (error) {
    if (error.name === 'AbortError') {
      statusText.textContent = '已取消下载';
    } else {
      statusText.textContent = `下载失败:${error.message}`;
    }
    progressBar.removeAttribute('value');
  } finally {
    // 中文注释:清空引用并恢复控件,避免下一次下载被旧状态挡住
    activeController = null;
    downloadButton.disabled = false;
    cancelButton.disabled = true;
  }
}

downloadButton.addEventListener('click', startDownload);
cancelButton.addEventListener('click', () => activeController?.abort());

示例中的官方资料入口可以直接复制:https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch。如果需要进一步确认流锁定、响应头或浏览器支持范围,可从该页面进入 Streams API 和 Headers 的说明。

上线前检查三个边界

  • 服务端是否真的返回可读的 Content-Length;没有它时,产品文案应接受不确定进度。
  • 跨域下载是否配置了允许脚本读取所需响应头,失败时不要把“读不到长度”误判为“没有文件”。
  • 请求取消、HTTP 非 2xx、读取中断和 Blob URL 释放是否都能回到可再次点击的状态。

常见问题

为什么进度条一直没有百分比?

最常见原因是响应没有可读取的 Content-Length,此时只能显示已接收字节。也可能是跨域响应头未暴露给脚本,先检查响应头读取权限。

读取 response.body 后还能再调用 response.blob() 吗?

不建议这样做。reader 会锁定流,流已经被消费后再用另一种方式读取会失败;应在同一次读取循环里收集字节块并构造 Blob。

为什么要延迟 revokeObjectURL?

临时 URL 需要先完成这次下载链接点击,再释放其引用。延迟到当前任务结束通常足够,长时间保留则会积累无用对象。

这套实现的关键不是让每次响应都出现漂亮的百分比,而是让界面状态忠实反映协议条件:有总长度就计算比例,没有总长度就报告已接收量;无论哪条分支结束,都释放请求和临时 URL 资源。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go HTTP 204响应携带正文时的处理边界Go HTTP 204响应携带正文时的处理边界
上一篇
Go HTTP 204响应携带正文时的处理边界
Go iter.Pull消费惰性迭代器后的停止与资源释放方案
下一篇
Go iter.Pull消费惰性迭代器后的停止与资源释放方案
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    129次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    198次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    143次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    118次使用
  • CMMLU中文大模型评估基准:功能、使用教程与应用场景解析
    CMMLU
    深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
    106次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码