当前位置:首页 > 文章列表 > 文章 > 前端 > Fetch API ReadableStream 的增量 JSON 解析

Fetch API ReadableStream 的增量 JSON 解析

来源:17golang原创 2026-10-28 13:41:46 0浏览 收藏

我第一次把大 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 边界”。

Response.body 字节块经过 TextDecoderStream 解码后按换行形成 NDJSON 记录并交给 JSON.parse 的结构说明图
图1:Response.body、UTF-8 文本块和 NDJSON 记录边界的结构说明图;这是原创静态结构图,不是截图或运行证据。

用 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(),并让渲染层丢弃已经失效的请求。不要把解析错误伪装成网络错误,也不要在没有协议约定时随意吞掉坏行。

buffer 保留半行并形成完整 JSON 记录后交给 JSON.parse,同时连接 SyntaxError 和 AbortController 取消边界的结构图
图2:增量解析中的缓冲区、错误分支和取消边界关系示意图;这是原创静态结构图,不是截图或运行证据。

常见问题

为什么不能直接对每个 ReadableStream chunk 调用 JSON.parse?
因为 chunk 是传输层分块,不保证是完整 JSON;它可能只包含对象前半段,也可能包含多条记录。

TextDecoderStream 能自动把 JSON 拆成一条条对象吗?
不能。它只负责字节到文本的解码,换行缓冲和 JSON.parse 仍由应用层完成。

普通 JSON 数组可以增量解析吗?
可以做,但不能只靠换行。若服务端可调整,NDJSON 更容易定义记录边界;若不能调整,则需要真正理解括号、字符串和转义的增量 tokenizer。

最小落地清单是什么?
服务端约定 NDJSON;客户端检查 response.ok;使用 TextDecoderStream;保留半行;流结束处理尾行;区分 SyntaxError 与 AbortError。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
JavaScript TextDecoderStream 怎样把流式字节转换成文本JavaScript TextDecoderStream 怎样把流式字节转换成文本
上一篇
JavaScript TextDecoderStream 怎样把流式字节转换成文本
Web Worker 与主线程 Transferable 数据传递
下一篇
Web Worker 与主线程 Transferable 数据传递
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    756次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    754次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    834次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    800次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    635次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码