Fetch API ReadableStream 的增量 JSON 解析
来源:17golang原创
时间:2026-10-28 13:41:46 385浏览 收藏
我第一次把大 JSON 接口改成流式读取时,误以为网络收到的每个 chunk 就是一条完整记录。实际情况是:Response.body 只提供字节流,分块位置由传输和浏览器决定,既可能把一个汉字拆开,也可能把两条记录合在一起。可靠的做法是让服务端输出 NDJSON(一行一个完整 JSON),浏览器先用 TextDecoderStream 解码,再按换行拼出完整记录,最后逐条调用 JSON.parse。
官方资料:https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
ReadableStream 解决“数据还在到达时先消费”,NDJSON 解决“什么时候算一条 JSON”,两者不能互相替代。
先把传输分块和 JSON 记录边界分开
fetch() 返回响应头后就能得到 Response,但 response.json() 会把响应体读完再解析。需要边到边处理时,应该读取 response.body。它返回的是字节流,读取到的 Uint8Array 不是 JSON 对象,也不保证对应一行。
因此文章只采用下面这种服务端格式,每行都是独立、完整的 JSON 值:
{"id":1,"message":"第一条"}
{"id":2,"message":"第二条"}
如果接口返回的是一个普通数组,例如 [{"id":1},{"id":2}],仅靠换行不能判断对象是否闭合;这时要么让服务端改成 NDJSON,要么引入专门的增量 JSON tokenizer。不要把“网络 chunk”误当成“JSON 边界”。

用 TextDecoderStream 保留跨块 UTF-8 字符
直接把每个字节块交给字符串转换,容易在多字节字符跨块时得到乱码。TextDecoderStream 会把字节流变成文本流,并在解码器内部保留尚未完成的 UTF-8 序列。对 Fetch 响应可以用 pipeThrough() 接起来:
async function* textChunks(url, signal) {
// 先判断 HTTP 状态;fetch 不会因为 404 自动 reject。
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
if (!response.body) {
throw new Error("响应没有可读取的 body");
}
// TextDecoderStream 默认按 UTF-8 解码,避免手动拼接半个汉字。
const stream = response.body.pipeThrough(new TextDecoderStream());
for await (const chunk of stream) {
// chunk 只是文本片段,仍然可能包含半行或多行。
yield chunk;
}
}
这里的关键是职责分离:解码器只负责“字节变成文本”,不会替你寻找换行,也不会替你调用 JSON.parse。如果接口的字符集不是 UTF-8,应先确认服务端响应约定,再选择对应的解码方式。
按换行积累完整记录并逐条 JSON.parse
我在实际页面里最容易漏掉的是“半行”。一个文本 chunk 可能在 JSON 中间结束,所以需要把它追加到 buffer,只在找到换行时取出完整行。下面的生成器把每条对象交给调用方:
async function* parseNdjson(url, signal) {
let buffer = "";
for await (const chunk of textChunks(url, signal)) {
buffer += chunk;
let newlineIndex;
// 一次处理当前缓冲区中的全部完整记录,剩余半行继续保留。
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (!line) continue;
try {
// 只有一整行到齐后才解析,避免把半个对象交给 JSON.parse。
yield JSON.parse(line);
} catch (error) {
// 给出原始行,便于定位服务端输出的哪条记录损坏。
throw new SyntaxError(`NDJSON 记录无效:${line}`);
}
}
}
// 最后一条记录可能没有换行,流结束时仍应处理它。
const tail = buffer.trim();
if (tail) {
yield JSON.parse(tail);
}
}
async function render(url, signal) {
// 消费端可以边收到边更新列表,不必等待整个响应结束。
for await (const item of parseNdjson(url, signal)) {
console.log(item.id, item.message);
}
}
trim() 只用于剔除行首尾的空白;真正的记录分隔仍然是换行。坏记录是否应该终止整个消费过程,要按业务决定:示例选择抛错,让调用方知道数据流已经不可信,而不是静默跳过。
补齐尾行、错误处理与取消
流式读取的代价是生命周期更长,错误也可能在响应进行到一半时发生。调用方至少要区分三类情况:HTTP 状态失败、记录 JSON 语法错误、用户主动取消。AbortController 可以把取消信号传给 fetch:
const controller = new AbortController();
async function loadMessages() {
try {
// signal 让关闭页面或点击“停止”时可以中断网络读取。
await render("/api/messages.ndjson", controller.signal);
} catch (error) {
if (error.name === "AbortError") {
console.log("用户取消了读取");
return;
}
// HTTP 错误和 SyntaxError 都应进入统一的错误提示或重试策略。
console.error("流式 JSON 读取失败", error);
}
}
loadMessages();
// 例如在组件卸载或用户点击停止时调用。
function stopLoading() {
controller.abort();
}
如果页面组件会频繁刷新,还要避免旧请求继续写入新页面;在卸载时调用 abort(),并让渲染层丢弃已经失效的请求。不要把解析错误伪装成网络错误,也不要在没有协议约定时随意吞掉坏行。

常见问题
为什么不能直接对每个 ReadableStream chunk 调用 JSON.parse?
因为 chunk 是传输层分块,不保证是完整 JSON;它可能只包含对象前半段,也可能包含多条记录。
TextDecoderStream 能自动把 JSON 拆成一条条对象吗?
不能。它只负责字节到文本的解码,换行缓冲和 JSON.parse 仍由应用层完成。
普通 JSON 数组可以增量解析吗?
可以做,但不能只靠换行。若服务端可调整,NDJSON 更容易定义记录边界;若不能调整,则需要真正理解括号、字符串和转义的增量 tokenizer。
最小落地清单是什么?
服务端约定 NDJSON;客户端检查 response.ok;使用 TextDecoderStream;保留半行;流结束处理尾行;区分 SyntaxError 与 AbortError。
-
130 收藏
-
495 收藏
-
198 收藏
-
414 收藏
-
163 收藏
-
330 收藏
-
114 收藏
-
292 收藏
-
116 收藏
-
302 收藏
-
120 收藏
-
349 收藏
-
261 收藏
-
342 收藏
-
426 收藏
-
110 收藏
-
198 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习