fetch 配合 textdecoder({ stream: true }) 和 readablestream 可实现平滑流式文本输出,解决 utf-8 多字节字符截断问题;需逐块解码、累积渲染,并对 sse 做额外解析。

Fetch 本身不直接处理流式文本的字符拼接,但配合 Response.body 的 ReadableStream、TextDecoder 和增量解码逻辑,就能实现类似“打字机”的平滑流式输出。关键在于避免按块粗暴拼接(如直接用 decoder.decode(chunk) 可能截断 UTF-8 多字节字符),而要用 TextDecoder 的流式解码能力(启用 { stream: true })并逐段消费。
用 TextDecoder({ stream: true }) 解决 UTF-8 截断问题
大模型返回的 SSE 或纯 text 流常以小 chunk(甚至单字节)分片传输。UTF-8 中中文、emoji 等字符占 2–4 字节,若 chunk 边界切在字符中间,普通 decode() 会出错或乱码。启用 stream: true 后,TextDecoder 会内部缓存未完成的字节序列,等后续 chunk 补齐后再输出完整字符。
示例做法:
- 创建解码器:
const decoder = new TextDecoder("utf-8", { stream: true }); - 每次读取 chunk 后调用
decoder.decode(chunk, { stream: true })(注意:第二个参数必须传{ stream: true },否则不启用流模式) - 不要用
decoder.decode(chunk)(无 stream 参数),它会强制 flush 缓存,导致中间截断字符被错误解析
用 ReadableStream.getReader() 按块读取并逐段解码
Fetch 返回的 Response.body 是一个 ReadableStream,需用 getReader() 获取流读取器,再循环 read() 获取 Uint8Array 块。
典型流程:
- 发起 fetch,检查
response.body是否存在 const reader = response.body.getReader();- 在
while(true)循环中const { done, value } = await reader.read(); - 若
value存在,用decoder.decode(value, { stream: true })解码;若done为 true,最后调用decoder.decode()(无参数)清空剩余缓存
拼接与打字效果:用 requestAnimationFrame 控制渲染节奏
解码后的文本是连续字符串片段,但直接拼到 DOM 会导致卡顿或闪烁。应累积片段、节流渲染:
- 维护一个
let accumulated = "",每次解码后追加:accumulated += decoded; - 用
requestAnimationFrame批量更新 DOM,避免高频textContent写入 - 如需“打字”动画感,可额外加
setTimeout或queueMicrotask控制每帧只追加少量字符(注意:这不是必须,真实流速由后端决定,UI 层模拟仅作体验优化)
处理 Server-Sent Events(SSE)需额外解析
若后端返回的是 SSE 格式(content-type: text/event-stream),每个 chunk 包含 data: ...\n\n,需手动提取 data: 后的内容再解码:
- 将 chunk 转为字符串:
new TextDecoder().decode(chunk)(此处可用非流式解码,因只是临时解析) - 按行分割,过滤掉空行和注释(以
:开头的行),提取data:行并去除前缀 - 把提取出的纯文本再交给流式
TextDecoder处理(如果该文本本身含 UTF-8 多字节,仍需流式解码)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











