当前位置:首页 > 文章列表 > 文章 > 前端 > ReadableStream 如何把 Fetch 响应分块显示到页面

ReadableStream 如何把 Fetch 响应分块显示到页面

来源:17golang原创 2026-09-12 11:07:33 0浏览 收藏

页面调用 Fetch 后一直等到请求结束才显示文字,通常不是“网络慢”这么简单:如果接口持续返回内容,就应该读取 response.body 这条 ReadableStream,并在每次拿到数据块后更新 DOM。关键是把 HTTP 状态、字节块和字符解码分开处理;中文被拆在两个块里时,不能直接把每个 Uint8Array 转成字符串。

要点速览
  • response.ok 只判断 HTTP 状态,真正读取前还要确认 response.body 存在。
  • 默认 reader 读到的是字节块,使用 TextDecoder 的流式模式才能保住 UTF-8 边界。
  • 结束时 flush 解码器,异常时恢复按钮,取消时调用 reader.cancel()

一、先确认响应成功且存在可读 body

先不要急着写递归读取。404、500 也可能带有响应内容,如果不检查 response.ok,错误页会被当成正常回答追加到页面。另一方面,某些响应没有 body,直接调用 getReader() 会在运行时出错。

Fetch response、Response.body、ReadableStream 与页面输出之间的前端边界关系图
图1:先看响应状态与 body 边界,再决定是否创建 ReadableStream reader。

下面的入口把状态检查放在读取之前,并清空旧内容;示例中的接口路径只是演示,实际项目替换成自己的流式接口。

const output = document.querySelector('#output');
const button = document.querySelector('#load');

async function showStream(url) {
  button.disabled = true; // 防止一次点击创建多个 reader
  output.textContent = '';

  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`); // 先报告状态,避免误显示错误页
  }
  if (!response.body) {
    throw new Error('响应没有可读 body'); // 没有流时走统一错误分支
  }

  return response.body;
}

二、用 getReader 逐块读取 Fetch 响应

response.body.getReader() 会把流锁定给这个 reader;同一条流不能同时交给另一个消费者。每次 read() 返回 donevalue,其中 value 通常是 Uint8Array。收到块后立即追加,页面就能看到逐步增长的内容。

async function readChunks(stream, output) {
  const reader = stream.getReader(); // reader 独占这条 ReadableStream
  const decoder = new TextDecoder('utf-8');

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break; // done 为 true 时不再处理 value

      output.textContent += decoder.decode(value, { stream: true });
    }
    output.textContent += decoder.decode(); // flush 末尾暂存的字节
  } finally {
    reader.releaseLock(); // 读取结束后释放锁,便于后续管理
  }
}

这里没有用 innerHTML,因为接口返回的文本不应直接解释成 HTML。若业务确实要渲染富文本,应先使用可信的 Markdown/HTML 清洗方案,不能把流式显示当成安全边界。

三、用 TextDecoder 保住 UTF-8 字符边界

网络块的边界不等于字符边界。一个汉字的 UTF-8 编码可能被拆开,第一块只到半个字符。每次都调用普通的 TextDecoder.decode(value),就可能出现替换字符;传入 { stream: true } 后,解码器会保留不完整字节,等下一块补齐。

ReadableStream 的 Uint8Array 字节块经过 TextDecoder 流式解码后进入页面文本缓冲的关系图
图2:TextDecoder 的流式状态负责把跨块字节拼回完整文字,再交给页面输出。

有些接口按换行分隔 JSON 或事件消息,这时还要在字符串缓冲里寻找换行符,不能假定一次 read() 恰好得到一条完整消息。解码和消息切分是两层问题:先保证字符完整,再按协议分帧。

四、把异常、结束和取消收在同一个出口

把入口、读取和按钮状态连起来,才能避免请求失败后按钮永久禁用。用户离开页面或点击停止时,调用 cancel() 表达“不再消费”,并在 finally 中恢复界面。

button.addEventListener('click', async () => {
  try {
    const stream = await showStream('/api/answer');
    await readChunks(stream, output);
  } catch (error) {
    output.textContent = `读取失败:${error.message}`; // 给用户可理解的失败反馈
  } finally {
    button.disabled = false; // 成功、失败、取消都恢复按钮
  }
});

async function stopReading(reader) {
  await reader.cancel('用户停止读取'); // 通知底层不再需要剩余数据
  reader.releaseLock(); // cancel 完成后再释放 reader 锁
}

生产代码通常会把当前 reader 保存到模块状态中,再由停止按钮调用取消函数;上面的片段重点展示收尾顺序。检查清单可以压缩成四项:状态先判定、body 再取 reader、字节按流式解码、结束后 flush 并释放锁。

相关问题

为什么页面还是一次性出现全部内容?

前端只是消费流,服务端、反向代理或压缩层仍可能缓冲响应。先确认接口确实分块发送,再检查代理的缓冲策略和响应头。

可以同时调用 response.text() 和 response.body 吗?

不建议。响应 body 只能被消费一次;选择流式 reader 后,就不要再用 text() 读取同一响应。

什么时候用 TextDecoderStream?

如果只需要把字节流转换成字符串流,可以用 response.body.pipeThrough(new TextDecoderStream());需要精细控制 reader、取消或协议分帧时,手动 reader 更直观。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Linux cgroup v2 io.max 如何限制设备带宽Linux cgroup v2 io.max 如何限制设备带宽
上一篇
Linux cgroup v2 io.max 如何限制设备带宽
Go bufio.Writer 如何用分层写入定位最终 Flush 错误
下一篇
Go bufio.Writer 如何用分层写入定位最终 Flush 错误
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    98次使用
  • OpenCompass大模型评测体系详解:功能、使用指南与应用场景
    OpenCompass
    OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
    28次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    253次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    180次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    114次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码