🌐 一、流式数据的本质
流式 = 数据分块到达 + 客户端逐块处理
- 服务器不等全部生成完就发送数据(chunked transfer)
- 客户端边收边处理,避免等待和内存爆炸
- 常见于:AI 对话、日志监控、实时通知、大文件下载/上传
📦 二、前端流式处理的完整方案矩阵
| 方案 | 协议/技术 | 是否原生 | 支持 POST | 自定义 Headers | 流式读取 | 适用场景 |
|---|---|---|---|---|---|---|
1. fetch + ReadableStream | HTTP chunked | ✅ | ✅ | ✅ | ✅✅✅ | 通用流式(OpenAI 风格) |
2. @microsoft/fetch-event-source | SSE (text/event-stream) | ❌(需安装) | ✅ | ✅ | ✅✅ | SSE 接口(LangChain/Ollama) |
3. 原生 EventSource | SSE | ✅ | ❌(仅 GET) | ❌ | ✅ | 简单同域 SSE(无认证) |
| 4. WebSocket | WebSocket | ✅ | N/A | ❌(握手后无 headers) | ✅ | 双向实时通信(聊天、协作) |
5. <video> / Media Source Extensions (MSE) | HLS/DASH/自定义流 | ✅ | ❌ | ❌ | ✅ | 视频/音频流播放 |
6. File API + File.slice() | 本地文件 | ✅ | N/A | N/A | ✅ | 大文件分片读取/上传 |
| 7. Web Transport (实验性) | QUIC/HTTP/3 | ⚠️(Chrome 97+) | ✅ | ✅ | ✅ | 未来高性能流(游戏、VR) |
🔍 三、详细方案解析
✅ 方案 1:fetch + ReadableStream(最通用)
适用后端:
- OpenAI 官方 API
- 自定义 chunked 文本流(非 SSE 格式)
- 返回纯文本或 JSONL(JSON Lines)
示例代码:
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: 'Hello' })
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
updateUI(chunk); // 实时渲染
}
✅ 优点:
- 完全控制流
- 支持任意格式
- 现代浏览器广泛支持
❌ 缺点:
- 需手动解析格式(如 JSONL)
- 无内置重连机制
✅ 方案 2:@microsoft/fetch-event-source(SSE 最佳实践)
适用后端:
- FastAPI with
StreamingResponse - LangChain / LlamaIndex
- Ollama
/api/generate - 自研 SSE 接口
示例代码:
import { fetchEventSource } from '@microsoft/fetch-event-source';
await fetchEventSource('/api/chat', {
method: 'POST',
body: JSON.stringify({ message: 'Hi' }),
headers: { Authorization: 'Bearer xxx' },
onmessage(event) {
if (event.data !== '[DONE]') {
const data = JSON.parse(event.data);
appendToken(data.token);
}
}
});
✅ 优点:
- 自动解析 SSE 格式
- 支持 POST + headers
- 内置重试 + AbortController
❌ 缺点:
- 需安装依赖
- 仅适用于标准 SSE 格式
⚠️ 方案 3:原生 EventSource(受限但简单)
适用场景:
- 同域 GET 请求
- 仅需 Cookie 认证
- 简单实时通知
示例:
const es = new EventSource('/events?userId=123');
es.onmessage = (e) => console.log(e.data);
❌ 限制:
- 无法传 Token
- 无法 POST
- 跨域需 CORS 配合
✅ 方案 4:WebSocket(双向流)
适用场景:
- 聊天应用
- 在线协作(如 Google Docs)
- 实时游戏
示例:
const ws = new WebSocket('wss://example.com/stream');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
renderMessage(data);
};
✅ 优点:
- 全双工通信
- 低延迟
- 支持二进制
❌ 缺点:
- 需维护长连接
- 服务端复杂度高
✅ 方案 5:Media Source Extensions (MSE)
适用场景:
- 自定义视频流(如监控)
- 动态拼接媒体片段
示例:
const video = document.querySelector('video');
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', () => {
const sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E"');
// 通过 fetch 获取视频 chunk 并 append
});
⚠️ 注意:仅用于音视频,非通用文本流。
✅ 方案 6:File API(本地流)
适用场景:
- 用户上传大文件(分片)
- 本地文件预览(如 CSV 分块解析)
示例:
input.onchange = (e) => {
const file = e.target.files[0];
const chunkSize = 1024 * 64;
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize);
const text = await chunk.text(); // 或 arrayBuffer()
processChunk(text);
}
};
🧪 方案 7:WebTransport(未来方向)
- 基于 QUIC,比 WebSocket 更快
- 支持可靠/不可靠流
- 目前仅 Chrome 支持,暂不推荐生产使用
🧩 四、如何选择?决策树
graph TD
A[需要流式读取?] -->|否| B[用 axios / fetch 常规请求]
A -->|是| C{后端返回什么格式?}
C -->|SSE text/event-stream| D{是否需要 POST 或 Token?}
D -->|是| E[用 @microsoft/fetch-event-source]
D -->|否| F[用原生 EventSource]
C -->|普通 chunked 流| G[用 fetch + ReadableStream]
C -->|双向通信| H[用 WebSocket]
C -->|音视频| I[用 MSE 或 <video>]
C -->|本地文件| J[用 File.slice()]
🛡️ 五、通用最佳实践(所有方案适用)
1. 内存安全
- 限制历史消息数量(如只保留最后 50 条)
- 使用虚拟滚动(
react-window) - 及时清理资源(
AbortController/close())
2. 用户体验
- 打字机效果(节流更新,避免卡顿)
- 加载状态提示(“正在思考...”)
- 支持“停止生成”按钮
3. 错误处理
- 网络中断自动重试(SSE)
- 优雅降级(如转为轮询)
- 用户友好错误提示
4. 性能优化
- 避免高频
setState(累积 buffer) - 使用
requestIdleCallback或setTimeout(0)让出主线程
📚 六、典型后端对应关系
| 后端技术 | 推荐前端方案 |
|---|---|
| OpenAI API | fetch + ReadableStream |
| Ollama / LM Studio | fetch-event-source |
| FastAPI StreamingResponse | fetch-event-source(若返回 SSE)或 fetch(若纯文本) |
| Spring Boot SseEmitter | fetch-event-source |
| Node.js res.write() | fetch(纯文本)或 fetch-event-source(若格式为 SSE) |
| WebSocket 服务 | 原生 WebSocket |
✅ 总结
| 场景 | 首选方案 |
|---|---|
| AI 流式对话(OpenAI 风格) | fetch + ReadableStream |
| AI 流式对话(SSE 格式) | @microsoft/fetch-event-source |
| 简单实时通知(同域) | 原生 EventSource |
| 双向实时交互 | WebSocket |
| 大文件处理 | File.slice() |
| 视频流 | MSE 或 <video> |
💡 记住:
- 浏览器中
axios不支持流式读取! - 流不会自动渲染,必须主动消费!
- 无限累积 = 内存泄漏,务必做限制!
如果你告诉我你的具体场景(比如“我用 FastAPI 返回 SSE,前端怎么接?”),我可以给出完整可运行代码!