前端如何调用 SSE 流式响应的接口?
- 标签:前端、编程导航
🏛 业务架构师
前端消费 SSE 有两种主流方式,按需求选:
1. EventSource(原生,简单但受限)
const es = new EventSource('/api/chat?msg=...');
es.onmessage = e => append(JSON.parse(e.data));局限:只支持 GET(参数塞 URL,长对话历史没法传)、不能自定义 header(token 鉴权麻烦)——只适合简单场景;
2. fetch + ReadableStream(生产主流)
const res = await fetch('/api/chat', {method:'POST', headers:{Authorization:...}, body:...});
const reader = res.body.getReader();
// 循环 reader.read(),按 "data: ...\n\n" 分隔解析事件块POST 传完整上下文、自定义鉴权头、可手动 abort(用户点"停止生成"→ AbortController.abort(),后端 onCompletion 清理模型订阅)——中断能力是 AI 应用的刚需,这是我选 fetch 的决定性理由。
工程要点(真实坑):
- 事件解析要处理分包:一个 SSE 事件可能被 TCP 拆成两截到达——按缓冲区累积 + \n\n 分隔符切分,不能假设一次 chunk 等于一条消息(这本质还是"流没有消息边界",和 TCP 粘包同构);
- 按事件类型分发:后端的事件契约(token/状态/完成/错误)前端分别处理——打字机渲染、工具状态图标、结束态切换;
- 断线处理:SSE 断开后模型流无法真正续传——保留已渲染内容 + 提示"重新生成",别做假续传;
- 渲染节奏:逐 token 直接触发重排会卡——攒 50ms 的 token 批量渲染,配合 Markdown 渐进解析。
一句话:fetch 流式读取 + 事件类型契约 + 中断控制,是 AI 前端消费流式接口的标准三件套。
🏗 系统架构师
前端消费 SSE 的标准实现(EventSource 与 fetch 两种方式):
方式一:EventSource(原生,简单场景):
javascript
const es = new EventSource('/api/chat/stream?conversationId=1');
es.onmessage = (e) => {
if (e.data === '[DONE]') { es.close(); return; }
appendToMessage(e.data); // 追加渲染打字机
};
es.onerror = () => { /* 自动重连或提示 */ };局限:只支持 GET、不能自定义 Header(鉴权 token 难带)——需要 token 的接口用 fetch。
方式二:fetch + ReadableStream(生产首选,支持 POST/头/中断):
javascript
const controller = new AbortController();
const resp = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` },
body: JSON.stringify({ conversationId, message }),
signal: controller.signal,
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 按 SSE 协议解析 "data: xxx\n\n",处理多行与事件类型
renderChunk(decoder.decode(value));
}
// 用户点"停止":controller.abort() → 服务端感知断开取消生成工程细节(体验分水岭):
- 分片解析要处理 chunk 边界切断(一个 data 包跨两次 read——维护 buffer 按分隔符切);
- 按 event 类型分流(message/done/error),done/error 显式结束;
- 打字机渲染配节流(requestAnimationFrame/16ms 合帧),高频 token 直接 append 会卡;
- 断线重连 + 基于最后消息 ID 的续传;AbortController 同步取消服务端生成(省钱)。
💻 全栈开发专家
首选 fetch + ReadableStream 手动解析,因为原生 EventSource 有两个硬伤:只支持 GET、不能带 Authorization header——而聊天接口是 POST 且要鉴权。
typescript
async function streamChat(payload: ChatRequest, onDelta: (t: string) => void) {
const resp = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json',
'Authorization': `Bearer ${token()}` },
signal: abortController.signal, // 停止生成
});
if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`);
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n'); // SSE 事件分隔符
buffer = events.pop() ?? ''; // 半包留在缓冲区!
for (const evt of events) {
const data = evt.replace(/^data: ?/m, ''); // 剥离 data: 前缀
if (data === '[DONE]') return;
if (data.startsWith('{"error"')) { throw parseError(data); }
onDelta(JSON.parse(data).content); // 逐 token 回调
}
}
}必须处理的四个边界(踩全了):
- chunk 半包:一个 SSE 事件可能被拆在两个网络 chunk 里——按
\n\n分割后最后一段留在 buffer 拼接下一轮,不做这个处理会随机丢 token; - 停止生成:AbortController abort,同时后端要感知连接断开停止调模型(省成本);
- 错误事件:约定 error 事件格式,中途失败要保留已生成内容 + 展示重试按钮;
- 页面切换:流进行中路由跳转要决定"后台续收"还是"中断",状态管理里以 conversationId 隔离。