🔥 AIGC 时代,别再用 EventSource 了!这个微软开源库才是 SSE 流式消息的正确打开方式
用过 ChatGPT、文心一言、通义千问吗?那个"打字机效果"的背后,就是 SSE(Server-Sent Events)。但浏览器原生的 EventSource API 槽点满满,今天介绍一个微软出品的替代方案 ——
@microsoft/fetch-event-source。
前言
2024-2026 年是 AIGC 爆发的时代。几乎所有 AI 对话产品都采用了 流式输出:用户提问后,答案像打字一样逐字出现,而不是等待几秒后一次性返回。
这背后的核心技术就是 SSE(Server-Sent Events) —— 服务端主动向客户端推送事件流。
但当你真正上手开发时,你会发现浏览器原生的 EventSource API 简直是个"半成品"……
一、原生 EventSource 的致命缺陷
浏览器内置的 EventSource 构造函数用起来很简单:
const sse = new EventSource('/api/chat/stream');
sse.onmessage = (event) => {
console.log(event.data);
};
但在 AIGC 场景下,它有 4 个致命问题:
❌ 1. 只能 GET,不能 POST
EventSource 只支持 GET 请求。而 AI 对话接口通常需要 POST 一个复杂的 JSON body(包含上下文消息、模型参数、系统 prompt 等),你根本塞不进 URL 里。
URL 长度限制普遍为 2000 字符,一段对话历史就能轻松超过。
❌ 2. 无法自定义 Headers
不能传 Authorization、不能传自定义 Content-Type、不能传 locale……在任何需要鉴权的系统中,这基本是"不可用"。
❌ 3. 错误处理形同虚设
连接断了?EventSource 会默默重试几次然后放弃。你无法区分是 401/403/500,也无法自定义重试策略。
❌ 4. 页面隐藏时的行为不可控
用户切换浏览器 Tab 后,连接行为不受开发者控制。
一句话总结:EventSource 是为"简单通知推送"设计的,不是为"AI 流式对话"设计的。
二、fetch-event-source:微软的解法
@microsoft/fetch-event-source 是微软开源的 SSE 客户端库,基于 Fetch API 重新实现了 EventSource 协议,完美解决上述所有问题。
- ⭐ GitHub 2.8k+ Stars
- 📦 MIT 协议
- 🏷️ 当前版本 v2.0.1
- 💻 TypeScript 编写,类型完备
安装
npm install @microsoft/fetch-event-source
# 或
pnpm add @microsoft/fetch-event-source
三、核心能力对比
| 能力 | 原生 EventSource | fetch-event-source |
|---|---|---|
| POST 请求 | ❌ | ✅ |
| 自定义 Headers | ❌ | ✅ |
| 自定义 Body | ❌ | ✅ |
| 错误状态码处理 | ❌ | ✅ |
| 自定义重试策略 | ❌ | ✅ |
| AbortController 取消 | ❌ | ✅ |
| 页面可见性管理 | 不可控 | ✅ 可配置 |
| 访问 Response 对象 | ❌ | ✅ |
四、快速上手
4.1 最简用法
import { fetchEventSource } from '@microsoft/fetch-event-source';
await fetchEventSource('/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token',
},
body: JSON.stringify({
messages: [{ role: 'user', content: '你好' }],
model: 'gpt-4',
stream: true,
}),
onmessage(event) {
const data = JSON.parse(event.data);
console.log(data.choices?.[0]?.delta?.content);
},
});
对比原生 EventSource,你可以:
- 发 POST 请求 ✅
- 带上 Authorization ✅
- 传 JSON Body ✅
4.2 优雅地取消请求
AI 生成到一半,用户点了"停止生成"?用 AbortController 即可:
const controller = new AbortController();
fetchEventSource('/api/chat/stream', {
method: 'POST',
body: JSON.stringify({ message: '写一篇 1000 字的文章' }),
signal: controller.signal,
onmessage(event) {
// 处理流式消息
},
});
// 用户点击「停止生成」
stopButton.onclick = () => controller.abort();
4.3 完善的错误处理
这是 AIGC 应用的重中之重。网络抖动、token 过期、服务端限流……都需要不同的处理策略:
class RetriableError extends Error {}
class FatalError extends Error {}
await fetchEventSource('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'hello' }),
async onopen(response) {
if (response.ok) {
return; // 连接成功
}
if (response.status === 401) {
// Token 过期,跳转登录
throw new FatalError('Unauthorized');
}
if (response.status === 429) {
// 限流,可以重试
throw new RetriableError('Rate limited');
}
if (response.status >= 400 && response.status < 500) {
// 客户端错误,不重试
throw new FatalError(`Client error: ${response.status}`);
}
// 服务端错误,重试
throw new RetriableError(`Server error: ${response.status}`);
},
onmessage(msg) {
if (msg.event === 'error') {
throw new FatalError(msg.data);
}
// 正常处理消息
},
onclose() {
// 服务端主动关闭连接
console.log('Connection closed by server');
},
onerror(err) {
if (err instanceof FatalError) {
throw err; // 抛出错误,停止重试
}
// 返回重试间隔(毫秒),或不返回使用默认策略
return 3000;
},
});
关键设计:
onerror中 throw 错误 = 停止重试,彻底断开onerror中 返回数字 = 等待指定毫秒后重试onerror中 不返回 = 使用默认重试策略
4.4 页面可见性控制
默认行为:用户最小化窗口或切换 Tab 时,库会 自动关闭连接,回来时带上 Last-Event-ID 重连。这对服务端很友好,避免维护无意义的长连接。
但在 AIGC 场景中,用户切 Tab 查资料时 AI 还在生成,你肯定不希望连接断掉:
await fetchEventSource('/api/chat/stream', {
// 关键配置:页面隐藏时保持连接
openWhenHidden: true,
onmessage(event) {
// 即使页面不可见也继续接收消息
},
});
💡 这个参数在 AIGC 场景中几乎是必开的。
五、AIGC 场景最佳实践
结合实际项目经验,总结几个关键实践:
5.1 封装统一的 SSE 请求层
建议将 fetchEventSource 封装成统一的工具函数,统一处理:
interface SSEConfig {
body?: any;
headers?: Record<string, string>;
method?: 'GET' | 'POST';
}
interface SSEHandlers {
onMessage?: (data: any, event?: string) => void;
onOpen?: (response: Response) => void;
onClose?: () => void;
onError?: (error: any) => void;
}
async function requestSSE(
url: string,
config: SSEConfig,
handlers: SSEHandlers,
abortController?: AbortController,
) {
const { body, headers = {}, method = 'POST' } = config;
await fetchEventSource(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
...headers,
},
body: body ? JSON.stringify(body) : undefined,
signal: abortController?.signal,
openWhenHidden: true,
async onopen(response) {
if (response.ok) {
handlers.onOpen?.(response);
return;
}
// 统一的 HTTP 错误处理
handleHttpError(response);
throw new Error(`HTTP ${response.status}`);
},
onmessage(event) {
if (!event.data) return;
let data;
try {
data = JSON.parse(event.data);
} catch {
data = event.data; // 纯文本场景兜底
}
handlers.onMessage?.(data, event.event);
},
onclose() {
handlers.onClose?.();
},
onerror(error) {
handlers.onError?.(error);
throw error; // AIGC 场景通常不自动重试
},
});
}
5.2 处理 FormData 上传 + 流式返回
多模态 AI 场景(上传图片/文件并流式返回分析结果)需要注意:
const formData = new FormData();
formData.append('file', file);
formData.append('question', '请分析这张图片');
await fetchEventSource('/api/multimodal/stream', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
// ⚠️ 不要手动设置 Content-Type!
// 浏览器会自动设置 multipart/form-data 并添加 boundary
},
body: formData, // 直接传 FormData,不要 JSON.stringify
openWhenHidden: true,
onmessage(event) {
// 处理流式响应
},
});
⚠️ 这是一个常见坑:FormData 不能 JSON.stringify,且不能手动设置
Content-Type。
5.3 SSE 消息的结构化处理
后端通常会通过 event 字段区分不同类型的消息:
onmessage(event) {
const data = JSON.parse(event.data);
switch (event.event) {
case 'message':
// 正常的文本 token
appendToConversation(data.content);
break;
case 'thinking':
// 思考过程(DeepSeek R1 等模型)
updateThinkingProcess(data.content);
break;
case 'tool_call':
// 工具调用(Function Calling)
showToolExecution(data);
break;
case 'done':
// 生成完成
finalizeResponse(data);
break;
case 'error':
// 业务错误
showErrorMessage(data.message);
break;
}
}
5.4 与 React 配合的 Hook 示例
function useSSEChat() {
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(false);
const controllerRef = useRef<AbortController | null>(null);
const send = useCallback(async (content: string) => {
// 取消上一次未完成的请求
controllerRef.current?.abort();
controllerRef.current = new AbortController();
setLoading(true);
try {
await requestSSE(
'/api/chat/stream',
{ body: { message: content } },
{
onMessage: (data) => {
setMessages(prev => {
// 追加 AI 回复内容
const last = prev[prev.length - 1];
return [
...prev.slice(0, -1),
{ ...last, content: last.content + data.content },
];
});
},
onClose: () => setLoading(false),
onError: () => setLoading(false),
},
controllerRef.current,
);
} catch {
setLoading(false);
}
}, []);
const stop = useCallback(() => {
controllerRef.current?.abort();
setLoading(false);
}, []);
return { messages, loading, send, stop };
}
六、原理简析
fetch-event-source 的核心原理并不复杂:
┌─────────────┐ fetch() ┌──────────────┐
│ Client │ ──────────▶ │ Server │
│ │ │ │
│ fetchEvent │ ◀────────── │ text/event │
│ Source() │ SSE Stream │ -stream │
└─────────────┘ └──────────────┘
│
▼
┌──────────────────────────┐
│ ReadableStream Reader │
│ ────────────────────── │
│ 1. 读取 response.body │
│ 2. 按 \n\n 分割事件 │
│ 3. 解析 event/data/id │
│ 4. 回调 onmessage │
└──────────────────────────┘
本质上就是:
- 用
fetch()发起请求(所以支持所有 fetch 参数) - 拿到
Response的ReadableStream - 逐块读取,按 SSE 协议(
\n\n分隔事件,data:/event:/id:前缀)解析 - 解析后回调
onmessage
七、与其他方案的对比
| 方案 | 适用场景 | 优缺点 |
|---|---|---|
| EventSource | 简单通知推送 | 简单但功能严重受限 |
| fetch-event-source | AIGC 流式对话 | 功能完整,API 优雅 |
| WebSocket | 双向实时通信 | SSE 场景下过于重量级 |
| 原生 fetch + ReadableStream | 极致定制 | 需要手动解析 SSE 协议 |
如果你的场景是服务端单向推送(AI 流式回答、实时日志、进度通知等),
fetch-event-source是最佳选择。 如果需要双向通信(协同编辑、实时聊天),请选择 WebSocket。
八、注意事项
- 该库已 3 年未更新(最后更新 2021 年),但 SSE 协议本身很稳定,功能上完全够用
- 打包体积很小,gzip 后仅约 2KB
- 不支持 IE,目标是 ES2017+ 的现代浏览器
- 如果用到旧版 Edge(< 79),需要 polyfill
TextDecoder
总结
在 AIGC 时代,SSE 是实现流式对话体验的核心技术。@microsoft/fetch-event-source 以极小的体积,优雅地解决了原生 EventSource 的所有痛点:
- ✅ 支持 POST + 自定义 Headers + Body
- ✅ 完善的错误处理和重试控制
- ✅ AbortController 取消支持
- ✅ 页面可见性智能管理
- ✅ TypeScript 友好
如果你正在做 AI 对话产品、流式数据展示、实时日志推送等功能,强烈推荐使用。
📌 GitHub:github.com/Azure/fetch… 📦 NPM:
npm install @microsoft/fetch-event-source📄 协议:MIT
如果这篇文章对你有帮助,欢迎点赞 👍 收藏 ⭐ 关注,我们下篇再见!