前言
做AI聊天应用时,最核心的交互就是"打字机效果"------一个字一个字地往外蹦。开发过程中,我们发现:用 axios 请求怎么等半天才出来?用 fetch 怎么也是一次性全返回?到底怎样才能实现真正的流式输出?
本文将从流式输出这个核心视角,彻底讲透四种请求方式的本质区别。
一、先说结论:一张表看懂全部区别
| 维度 | 普通 Fetch | Fetch 流式 | Axios | SSE |
|---|---|---|---|---|
| 能否做流式输出 | ❌ 不能 | ✅ 原生支持 | ❌ 不能(伪实现) | ✅ 原生支持 |
| 数据接收方式 | 一次性接收完整响应 | 逐块接收分块数据 | 一次性接收完整响应 | 服务端逐块推送 |
| 响应体类型 | res.json()/res.text() 一次性解析 |
response.body 是 ReadableStream |
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 的组合
- 服务端:开启 HTTP Chunked 编码,将数据切分成多个独立的"数据块",逐个发送
- 浏览器 :Fetch 拿到响应后,
response.body暴露为ReadableStream可读流 - 前端 :通过流的读取器(
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.body 是 ReadableStream |
| 内存占用 | 完整数据存入内存,大文件易溢出 | 仅占用当前数据块的内存 |
| 处理时机 | 必须等待全部返回 | 边接收、边处理 |
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 | 以上方案均不支持双向 |
写在最后
记住三句话: