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

52 阅读6分钟

前言

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

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


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

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

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

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

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 核心区别

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

3.4 适用场景

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

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

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

4.1 为什么不行?

javascript

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

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

4.2 那 onDownloadProgress 呢?

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

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

// 客户端:极其简单
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

┌─────────────────────────────────────────────────────────────┐
│                    你的需求是什么?                          │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
              ┌───────────────────────────┐
              │   需要服务端→客户端的实时推送? │
              └───────────────────────────┘
                     │                │
                   是│                │否
                     ▼                ▼
         ┌─────────────────┐  ┌─────────────────┐
         │  需要 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 请求,优先选它