普通Fetch、Fetch流式、Axios、SSE:谁才是流式输出的正确操作?

前言

做AI聊天应用时,最核心的交互就是"打字机效果"------一个字一个字地往外蹦。开发过程中,我们发现:用 axios 请求怎么等半天才出来?用 fetch 怎么也是一次性全返回?到底怎样才能实现真正的流式输出?

本文将从流式输出这个核心视角,彻底讲透四种请求方式的本质区别。


一、先说结论:一张表看懂全部区别

维度 普通 Fetch Fetch 流式 Axios SSE
能否做流式输出 ❌ 不能 原生支持 ❌ 不能(伪实现) 原生支持
数据接收方式 一次性接收完整响应 逐块接收分块数据 一次性接收完整响应 服务端逐块推送
响应体类型 res.json()/res.text() 一次性解析 response.bodyReadableStream response.data 一次性返回 EventSource.onmessage 逐条接收
通信方向 单向(请求→响应) 单向(请求→响应) 单向(请求→响应) 单向(服务端→客户端)
协议基础 HTTP HTTP HTTP HTTP
内存占用 完整数据存入内存 仅占当前块内存 完整数据存入内存 逐条接收,内存友好
处理时机 必须等待响应完全返回 边接收、边处理 必须等待响应完全返回 边接收、边处理
自动重连 内置自动重连
适用场景 普通 REST API 大文件下载、AI流式响应、实时日志 普通 REST API AI流式响应、实时通知、股票行情

二、普通 Fetch:看起来像流式,其实不是

javascript

javascript 复制代码
// ❌ 这不是流式!这是一次性接收
const response = await fetch('/api/chat');
const data = await response.text(); // 这里会阻塞,直到全部数据到达
console.log(data); // 一次性打印全部

普通 Fetch 的工作方式 :发请求 → 等待服务端完整响应 → 一次性解析 → 返回数据

核心问题response.json()response.text() 都会等待整个响应体完全下载完成后,才将结果返回给你。在这之前,你什么都做不了

适用场景:普通的 REST API 调用,数据量小,不需要实时反馈。


三、Fetch 流式:真正的流式输出王者

这才是实现 ChatGPT 打字机效果的正确姿势。

3.1 核心原理

Fetch 流式的底层是 HTTP 分块传输编码(Chunked Transfer Encoding)+ 浏览器 Streams API 的组合

  1. 服务端:开启 HTTP Chunked 编码,将数据切分成多个独立的"数据块",逐个发送
  2. 浏览器 :Fetch 拿到响应后,response.body 暴露为 ReadableStream 可读流
  3. 前端 :通过流的读取器(reader)逐块读取数据

3.2 核心代码

javascript

javascript 复制代码
async function streamFetch(url) {
  // 1. 发起请求(注意:没有 await res.text()!)
  const response = await fetch(url);
  
  // 2. 校验响应是否支持流式
  if (!response.ok || !response.body) {
    throw new Error('不支持流式响应');
  }
  
  // 3. 获取流的读取器
  const reader = response.body.getReader();
  const decoder = new TextDecoder('utf-8', { stream: true });
  
  try {
    // 4. 循环逐块读取(核心逻辑)
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      // 5. 实时处理当前数据块
      const chunkText = decoder.decode(value);
      console.log('实时输出:', chunkText);
      // 追加到页面,实现打字机效果
      document.getElementById('content').textContent += chunkText;
    }
  } finally {
    // 6. 释放读取器,避免内存泄漏
    reader.releaseLock();
  }
}

3.3 核心区别

维度 普通 Fetch Fetch 流式
数据接收 一次性接收完整响应 逐块接收服务端的分块数据
响应体类型 res.json()/res.text() response.bodyReadableStream
内存占用 完整数据存入内存,大文件易溢出 仅占用当前数据块的内存
处理时机 必须等待全部返回 边接收、边处理

3.4 适用场景

  • 大文件下载(100MB+,避免内存溢出)
  • AI 大模型流式响应(打字机效果)
  • 实时日志、后台任务进度推送
  • 超大 JSON 逐块解析渲染

四、Axios:为什么它做不了真正的流式输出?

这是很多人的误区:Axios 底层基于 XMLHttpRequest,设计逻辑是"全部拿回来再给你"

4.1 为什么不行?

javascript

javascript 复制代码
// ❌ Axios 只能这样写
const response = await axios.post('/api/chat', { prompt: 'hi' });
console.log(response.data); // 一次性全部返回,无法逐字输出

Axios 的 response.data 只有在服务端完整响应后才会被赋值。在数据完全到达之前,你拿不到任何内容。

4.2 那 onDownloadProgress 呢?

Axios 确实提供了 onDownloadProgress 进度监听:

javascript

javascript 复制代码
axios.post('/api/chat', data, {
  onDownloadProgress: (progressEvent) => {
    console.log('下载进度:', progressEvent.loaded / progressEvent.total);
  }
});

但这个方法只能告诉你"下载了多少字节" ,你无法拿到具体的内容逐块渲染。即使通过某些 hack 手段获取 responseText 的累积值,也会面临:

  • 无法按 \n\n 边界切分数据
  • 中文等多字节字符可能被截断导致乱码
  • 需要手动做增量对比,极其繁琐

4.3 结论

Axios 不适合做流式输出。它是为 RESTful API(一次性返回 JSON)设计的,如果你想做 AI 聊天流式效果,请直接使用 Fetch 流式。


五、SSE(Server-Sent Events):流式输出的另一种优雅方案

SSE 是 HTML5 规范的一部分,专门为"服务器向客户端推送数据"而设计

5.1 核心用法

javascript

javascript 复制代码
// 客户端:极其简单
const eventSource = new EventSource('/api/chat');

eventSource.onmessage = (event) => {
  // 每收到一条数据,实时处理
  console.log('收到数据:', event.data);
  document.getElementById('content').textContent += event.data;
};

eventSource.onerror = (error) => {
  console.error('连接出错', error);
};

5.2 SSE 的核心优势

  • API 极其简单new EventSource(url) 三行代码搞定
  • 自动重连 :连接意外断开时,浏览器会自动尝试重连(默认间隔3秒),无需手动处理
  • 协议友好:基于 HTTP,更容易穿透防火墙和代理

5.3 SSE vs Fetch 流式:怎么选?

对比维度 Fetch 流式 SSE
请求方式 支持 GET/POST,可带自定义 Header 仅支持 GET(无法带自定义 Header)
自动重连 需手动实现 ✅ 内置
实现复杂度 中等(需手动操作 ReadableStream) (EventSource API 开箱即用)
灵活性 高(可结合 AbortController 取消) 中(关闭需手动调用 .close()

5.4 适用场景

  • AI 对话流式响应(ChatGPT 风格)
  • 实时通知、消息推送
  • 股票行情、体育比分更新

六、总结:到底该怎么选?

text

css 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    你的需求是什么?                          │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
              ┌───────────────────────────┐
              │   需要服务端→客户端的实时推送? │
              └───────────────────────────┘
                     │                │
                   是│                │否
                     ▼                ▼
         ┌─────────────────┐  ┌─────────────────┐
         │  需要 POST +     │  │  普通 REST API   │
         │  自定义 Header?  │  │  一次性返回数据   │
         └─────────────────┘  └─────────────────┘
              │        │              │
            是│        │否            ▼
              ▼        ▼      ┌─────────────┐
      ┌──────────┐ ┌──────┐  │  普通 Fetch  │
      │Fetch流式 │ │ SSE  │  │  或 Axios   │
      └──────────┘ └──────┘  └─────────────┘

决策指南

场景 推荐方案 理由
AI 对话(需 POST 发送对话历史) Fetch 流式 支持 POST + 自定义 Header,灵活控制
AI 对话(仅需 GET) SSE 代码最简单,自动重连
大文件下载(100MB+) Fetch 流式 内存占用恒定,避免页面卡顿
实时通知/股票行情 SSE 服务端单向推送,开箱即用
普通 REST API 调用 普通 Fetch 或 Axios 杀鸡不用牛刀
需要双向通信(如在线游戏) WebSocket 以上方案均不支持双向

写在最后

记住三句话:

  1. Fetch 流式的核心是 response.body 这个 ReadableStream,而不是 response.json()response.text()
  2. Axios 做不了真正的流式输出,它骨子里就是"一次性全部返回"的设计
  3. SSE 是流式输出的"傻瓜式"方案,如果业务允许 GET 请求,优先选它
相关推荐
奇奇怪怪的1 小时前
多模态 RAG:图片与表格检索
前端
犇驫聊AI1 小时前
Claude Code 用了大半年才悟出来的 6 个技巧,第 3 个直接省一半 Token
前端·claude
团团崽_七分甜1 小时前
Spring Data JPA 从入门到实战完整指南
前端
我是大卫2 小时前
React源码解析-第一部分:宏观架构
前端·react.js·源码
我是大卫2 小时前
React源码解析-第二部分:更新与调度
前端·react.js·源码
狸礼li2 小时前
web网站设计作品--第二大脑
前端
2603_955279702 小时前
C++23新特性在CLion中的实战体验
前端·javascript·c++23
梨子同志2 小时前
HTTP
前端
kidding7232 小时前
旋转大转盘小程序
前端·css·微信小程序·小程序·前端框架