前言
在 AI 大模型火热的今天,流式输出(Streaming)已成为标配。虽然浏览器原生提供了 EventSource (SSE),但在复杂的业务实战中,它却显得力不从心。本文将带你深度剖析 fetch-event-source 的底层实现,看看它是如何突破原生限制,优雅实现流式交互的。
一、 为什么原生 EventSource 走到了尽头?
原生 EventSource 在 AI 聊天场景中有两个“死穴”:
- 方法受限:只能发送 GET 请求。AI 聊天往往需要携带庞大的上下文(Context),URL 长度限制是无法逾越的障碍。
- 鉴权困境:无法自定义 Header。在需要通过
Authorization传递 Token 的现代 Web 应用中,这非常致命。
fetch-event-source 的出现,本质上是给 fetch 套上了一层 SSE 的协议外壳,完美继承了 fetch 的灵活性。
二、 核心原理:基于 ReadableStream 的流式解析
fetch-event-source 的核心魔法在于利用了 fetch 返回值中的 Response.body。它是一个 ReadableStream(可读流),允许我们在数据还没全部到达时,就开始处理已经“流”进来的字节块。
1. 协议头强制对齐
要模拟 SSE,请求头必须严格遵守规范:
Accept: text/event-stream:告知后端我们需要流式响应。Cache-Control: no-cache:禁用缓存,确保实时性。Connection: keep-alive:保持长连接。
2. 状态机解析逻辑
由于 SSE 格式具有高度可预测性(以 \n 分隔行,以 \n\n 分隔消息块),我们可以通过一个简单的状态机进行逐行扫描:
data:开头 -> 暂存数据片段。event:开头 -> 记录事件类型。retry:开头 -> 更新客户端的重连等待时间。- 空行 (
\n\n) -> 表示一条消息解析完成,触发onmessage回调。
三、 手写一个简易版
理解原理最好的方式就是复刻它。以下是基于 fetch 和 TextDecoder 的核心实现逻辑:
async function fetchEventSource(url, options) {
const { signal, onopen, onmessage, onerror, retryDelay = 1000 } = options;
let retryCount = 0;
// 1. 循环处理(失败重试)
while (!signal.aborted) {
try {
const response = await fetch(url, {
method: 'POST', // 突破 GET 限制,支持 POST 发送上下文
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
...options.headers,
},
body: JSON.stringify(options.body),
signal,
});
// 2. 响应合法性校验
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
if (!response.headers.get('Content-Type')?.includes('text/event-stream')) {
throw new Error('Invalid Content-Type, expected text/event-stream');
}
onopen?.({ response });
// 3. 读取流式响应体 (核心)
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码二进制数据并追加到缓冲区
buffer += decoder.decode(value, { stream: true });
// 4. 按 SSE 规范拆分消息块 (\n\n)
let parts = buffer.split('\n\n');
buffer = parts.pop(); // 最后一个可能是残缺的,留到下一轮处理
for (const part of parts) {
// 这里解析 data: event: 等字段
const parsed = parseSSEPart(part);
onmessage?.(parsed);
}
}
await reader.releaseLock();
if (signal.aborted) break;
throw new Error('Connection closed by server');
} catch (error) {
// 5. 错误处理与指数退避重连
const retry = onerror?.(error) ?? true;
if (!retry || signal.aborted) break;
const delay = retryDelay * Math.pow(2, retryCount);
await new Promise(resolve => setTimeout(resolve, delay));
retryCount++;
}
}
}
四、 总结
fetch-event-source 并不是魔法,它只是站在了 fetch 和 ReadableStream 的肩膀上,通过手动实现 SSE 协议解析,解决了原生 API 的痛点。在 AI 对话应用中,它是实现实时、鉴权、高扩展性流式输出的最佳实践。