一、什么是 SSE?为什么 AI 流式必用它?
1. 定义
SSE(Server-Sent Events) 是浏览器原生支持的技术,基于 HTTP 长连接,服务端可以单向推送数据到前端。它无需任何第三方库,浏览器自带 EventSource 对象。
2. 为什么 AI 打字机必须用 SSE?
| 特性 | 优势 |
|---|---|
| 单向推送 | AI 只需要服务端往前端吐文字,不需要前端实时回传 |
| 原生支持 | 浏览器自带 EventSource,零依赖 |
| 自动重连 | 断网后自动重连,比 WebSocket 简单太多 |
| 轻量 | 比 WebSocket 更小、更快、更稳定 |
| 文本流友好 | 完美适配 LLM 逐字返回的场景 |
3. SSE vs WebSocket
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 仅服务端→前端 | 双向 |
| 协议 | HTTP | ws/wss |
| 重连 | 自动 | 手动实现 |
| 数据格式 | 纯文本 | 文本/二进制 |
| 适用场景 | AI 流式输出、通知、日志 | 聊天、游戏、协同 |
结论:AI 流式打字机 = SSE 最优解。
二、SSE 实现打字机的核心原理
整个流程可以拆分为五步:
- 前端创建
EventSource连接后端 SSE 接口 - 后端以
stream: true持续返回 JSON 片段 - 前端逐块接收、增量渲染 → 形成打字机效果
- 监听错误 → 自动重连
- 接收结束标志 → 关闭连接
核心思想很简单:不是覆盖,是追加。
三、完整可运行代码
前端核心代码(原生 JS)
<!DOCTYPE html>
<html>
<body>
<div id="content"></div>
<script>
const content = document.getElementById('content');
let source;
function initSSE() {
// 关闭旧连接
if (source) source.close();
// 创建新连接
source = new EventSource('/api/ai/stream', {
withCredentials: true
});
// 接收消息 → 打字机效果
source.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
const text = data.content || '';
// 核心:增量追加,不是覆盖
content.innerHTML += text;
} catch (err) {}
};
// 连接状态
source.onopen = () => console.log('SSE 连接成功');
source.onclose = () => console.log('SSE 正常关闭');
// 最重要:错误处理 + 断线重连
source.onerror = (err) => {
console.error('SSE 错误', err);
source.close();
setTimeout(initSSE, 1000);
};
}
initSSE();
</script>
</body>
</html>
React Hooks 版本(面试高频)
import { useState, useEffect, useCallback } from 'react';
export function useAIStream(url: string) {
const [text, setText] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
useEffect(() => {
let source: EventSource;
let cancelled = false;
const connect = () => {
if (source) source.close();
source = new EventSource(url);
setIsStreaming(true);
source.onmessage = (e) => {
if (cancelled) return;
try {
const data = JSON.parse(e.data);
if (data.done) {
setIsStreaming(false);
source.close();
return;
}
// 函数式更新,避免闭包陷阱
setText(prev => prev + (data.content || ''));
} catch (err) {}
};
source.onerror = () => {
source.close();
setIsStreaming(false);
if (!cancelled) {
setTimeout(connect, 1000);
}
};
};
connect();
return () => {
cancelled = true;
source?.close();
};
}, [url]);
return { text, isStreaming };
}
四、逐行精讲(面试能讲出来就是高手)
1. setText(prev => prev + data.content)
使用函数式更新而非 setText(text + data.content),原因:
- text 是闭包中的旧值
- 高频 SSE 消息可能造成状态更新丢失
- 函数式更新确保每次基于最新值追加
2. 断线重连的黄金法则
source.onerror = () => {
source.close(); // 必须关闭旧连接
if (!cancelled) { // 组件未卸载才重连
setTimeout(connect, 1000); // 1s 间隔避免雪崩
}
};
三个关键点:
- 先 close 再重连,否则内存泄漏
- 检查 cancelled,避免组件卸载后重连
- 延迟重连,避免服务端压力雪崩
五、常见问题与解决方案
Q1:消息乱序怎么办?
SSE 基于 HTTP 长连接,TCP 保证顺序,不会乱序。如果你遇到乱序,检查后端是否用了异步非阻塞写。
Q2:大量用户同时连接,服务端撑不住?
- 连接池:后端用异步框架(FastAPI/Fiber),单机可承载万级连接
- Nginx 代理:配置
proxy_buffering off,确保流式不缓存 - 负载均衡:SSE 要求 sticky session,同一客户端始终发到同一后端
Q3:如何处理跨域?
new EventSource(url, { withCredentials: true });
后端设置 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials。
六、总结
- SSE 是浏览器原生技术,无需第三方库
- 核心思想:增量追加,而非整体替换
- 断线重连是生产环境必备能力
- React 中务必使用函数式
setState - 配合
TransformStream可处理更复杂的流式场景
掌握了这些,AI 流式打字机效果对你来说就是小菜一碟。
Comments 留言讨论
还没有评论,来抢个沙发,聊聊你的看法~