前端 SSE 实战指南:从 EventSource 到 Fetch Streams
Server-Sent Events (SSE) 是一种基于 HTTP 的服务端推送技术,允许服务器通过持久化的 HTTP 连接向客户端单向推送数据。相比 WebSocket 的全双工通信,SSE 更轻量、更简单,特别适合"服务端推送、客户端接收"的场景,如实时通知、AI 流式输出、进度条更新等。
本文将从实际开发出发,根据接口的 HTTP 方法(GET / POST)分别讲解最合适的 SSE 前端实现方案,并提供可直接运行的代码示例。
目录
- [一、SSE 协议基础](#一、SSE 协议基础 "#%E4%B8%80sse-%E5%8D%8F%E8%AE%AE%E5%9F%BA%E7%A1%80")
- [二、GET 接口 → EventSource:为什么它是天然之选](#二、GET 接口 → EventSource:为什么它是天然之选 "#%E4%BA%8Cget-%E6%8E%A5%E5%8F%A3--eventsource%E4%B8%BA%E4%BB%80%E4%B9%88%E5%AE%83%E6%98%AF%E5%A4%A9%E7%84%B6%E4%B9%8B%E9%80%89")
- [三、POST 接口 → fetch + ReadableStream:突破 EventSource 的限制](#三、POST 接口 → fetch + ReadableStream:突破 EventSource 的限制 "#%E4%B8%89post-%E6%8E%A5%E5%8F%A3--fetch--readablestream%E7%AA%81%E7%A0%B4-eventsource-%E7%9A%84%E9%99%90%E5%88%B6")
- 四、方案对比与选型建议
- 五、常见陷阱与最佳实践
- [六、封装一个通用的 SSE 客户端](#六、封装一个通用的 SSE 客户端 "#%E5%85%AD%E5%B0%81%E8%A3%85%E4%B8%80%E4%B8%AA%E9%80%9A%E7%94%A8%E7%9A%84-sse-%E5%AE%A2%E6%88%B7%E7%AB%AF")
一、SSE 协议基础
SSE 的核心是 text/event-stream 这一 MIME 类型。服务端响应的格式如下:
vbnet
data: 第一条消息\n
\n
data: 第二条消息\n
\n
event: progress\ndata: 50%\n\n
event: result\ndata: {"answer": "你好"}\n\n
关键规则:
- 每条消息由
field: value\n组成,消息之间用空行\n分隔 - 支持的字段:
data(数据体)、event(事件类型)、id(消息 ID)、retry(重连间隔) data可以多行,多行data之间用\n拼接- 连接断开后,浏览器会自动重连(这是 SSE 的重要特性)
二、GET 接口 → EventSource:为什么它是天然之选
2.1 EventSource 是什么
EventSource 是浏览器原生提供的 SSE API,属于 HTML5 标准的一部分。它的设计目标就是消费 text/event-stream 格式的响应流。
2.2 为什么 GET + EventSource 是最佳组合
| 论点 | 说明 |
|---|---|
| 协议匹配 | SSE 标准本身就基于 GET 语义设计------客户端发起一个长连接请求,服务端持续推送数据。EventSource 内部就是发一个 GET 请求 |
| 自动重连 | EventSource 内置断线重连机制,连接中断后会自动按 retry 间隔或默认 3 秒重连,且自动携带 Last-Event-ID 请求头,服务端可据此续传 |
| API 简洁 | 只需 new EventSource(url) 即可建立连接,无需手动处理流解析、换行分割、事件分发等底层逻辑 |
| 浏览器兼容 | 所有现代浏览器均支持(IE 除外,但 IE 已停止支持) |
| 语义正确 | GET 请求是幂等、安全的,适合"获取数据"的语义;SSE 本质上就是"获取一个持续更新的数据流" |
2.3 完整示例
基础用法
javascript
const evtSource = new EventSource('/api/notifications');
// 监听默认的 message 事件
evtSource.onmessage = (event) => {
console.log('收到消息:', event.data);
};
// 监听自定义事件类型
evtSource.addEventListener('progress', (event) => {
const percent = event.data;
console.log(`进度: ${percent}%`);
});
evtSource.addEventListener('result', (event) => {
const data = JSON.parse(event.data);
console.log('最终结果:', data);
// 收到最终结果后关闭连接
evtSource.close();
});
// 连接错误处理
evtSource.onerror = (event) => {
if (evtSource.readyState === EventSource.CONNECTING) {
console.log('连接断开,正在自动重连...');
} else if (evtSource.readyState === EventSource.CLOSED) {
console.log('连接已关闭');
}
};
带 URL 参数的 GET 请求
javascript
// 通过 URL 查询参数传递数据
const params = new URLSearchParams({
topic: 'weather',
city: 'shenzhen'
});
const evtSource = new EventSource(`/api/stream?${params.toString()}`);
evtSource.addEventListener('weather-update', (event) => {
const weather = JSON.parse(event.data);
updateWeatherUI(weather);
});
手动控制连接生命周期
javascript
class SSEClient {
constructor(url) {
this.url = url;
this.eventSource = null;
}
connect() {
this.eventSource = new EventSource(this.url);
this.eventSource.onopen = () => {
console.log('SSE 连接已建立');
};
this.eventSource.onmessage = (event) => {
// 解析 JSON 数据
try {
const data = JSON.parse(event.data);
this.onReceiveData(data);
} catch {
this.onReceiveData(event.data);
}
};
this.eventSource.onerror = () => {
console.log('SSE 连接出错,浏览器将自动重连');
};
}
// 子类重写此方法处理数据
onReceiveData(data) {
console.log('data:', data);
}
disconnect() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
console.log('SSE 连接已主动关闭');
}
}
}
// 使用
const client = new SSEClient('/api/live-updates');
client.onReceiveData = (data) => {
document.getElementById('output').textContent = data.text;
};
client.connect();
// 不再需要时关闭
// client.disconnect();
2.4 EventSource 的局限性
| 局限 | 影响 |
|---|---|
| 仅支持 GET | 无法发送 POST/PUT/DELETE 请求,无法携带请求体 |
| 无法自定义请求头 | 无法设置 Authorization、Content-Type 等请求头(除非通过 URL 参数传递 token,但不安全) |
| 无法控制请求体 | 无法发送 JSON body |
| 错误处理有限 | 只能知道连接出错,无法获取 HTTP 状态码等详细信息 |
正是这些局限性,使得当接口为 POST 方法时,我们需要
fetch + ReadableStream方案。
三、POST 接口 → fetch + ReadableStream:突破 EventSource 的限制
3.1 为什么需要 POST
在实际开发中,POST 接口的 SSE 场景非常常见:
- AI 对话接口:需要将用户的 prompt 作为 JSON body 发送,服务端流式返回生成内容
- 复杂查询:筛选条件太多,URL 参数放不下或不够安全
- 需要鉴权 :请求头中需要携带
Authorization: Bearer xxx - 批量操作进度:需要发送操作 ID 或参数,服务端推送进度
3.2 fetch + ReadableStream 方案原理
核心思路:使用 fetch 发送 POST 请求,然后通过 response.body.getReader() 逐块读取响应流,手动解析 SSE 格式的数据。
3.3 完整示例
基础版:流式读取 AI 对话
javascript
async function chatStream(prompt) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token-here'
},
body: JSON.stringify({ prompt })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = ''; // 用于处理不完整的消息块
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 将 Uint8Array 解码为字符串,拼接到缓冲区
buffer += decoder.decode(value, { stream: true });
// 按双换行分割消息(SSE 协议标准)
const messages = buffer.split('\n\n');
// 最后一段可能不完整,保留在 buffer 中
buffer = messages.pop();
for (const msg of messages) {
if (!msg.trim()) continue;
// 解析每行字段
let data = '';
let eventType = '';
for (const line of msg.split('\n')) {
if (line.startsWith('data:')) {
data += line.slice(5).trimStart();
} else if (line.startsWith('event:')) {
eventType = line.slice(6).trim();
}
}
if (data === '[DONE]') {
console.log('流结束');
return;
}
if (data) {
try {
const parsed = JSON.parse(data);
process.stdout.write(parsed.chunk || data);
} catch {
console.log(data);
}
}
}
}
}
// 使用
chatStream('请用中文写一首关于代码的诗');
进阶版:完整 SSE 解析器 + AbortController
javascript
/**
* SSE 流式请求客户端
* 支持 POST 请求、自定义请求头、事件类型分发、中断控制
*/
class SSEStreamClient {
constructor(options = {}) {
this.baseURL = options.baseURL || '';
this.defaultHeaders = options.headers || {};
this.controller = null;
}
/**
* 发起 SSE 流式请求
* @param {string} url - 请求地址
* @param {object} body - 请求体
* @param {object} options - 可选配置
* @param {object} options.headers - 额外请求头
* @param {function} options.onMessage - 默认消息回调
* @param {function} options.onEvent - 自定义事件回调 (event, data) => void
* @param {function} options.onError - 错误回调
* @param {function} options.onComplete - 流结束回调
*/
async connect(url, body, options = {}) {
// 创建 AbortController 用于中断请求
this.controller = new AbortController();
const headers = {
'Content-Type': 'application/json',
...this.defaultHeaders,
...options.headers
};
try {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'POST',
headers,
body: JSON.stringify(body),
signal: this.controller.signal
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
await this._readStream(response, options);
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求已被取消');
} else {
options.onError?.(error);
throw error;
}
} finally {
this.controller = null;
}
}
/**
* 中断当前请求
*/
abort() {
this.controller?.abort();
}
/**
* 读取并解析 SSE 流
*/
async _readStream(response, options) {
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let lastEventId = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 按双换行分割消息
const parts = buffer.split('\n\n');
buffer = parts.pop() || '';
for (const part of parts) {
if (!part.trim()) continue;
const parsed = this._parseSSEMessage(part);
if (!parsed) continue;
lastEventId = parsed.id || lastEventId;
// 分发事件
if (parsed.event && parsed.event !== 'message') {
options.onEvent?.(parsed.event, parsed.data, parsed.id);
} else {
options.onMessage?.(parsed.data, parsed.id);
}
}
}
// 处理缓冲区中剩余的数据
if (buffer.trim()) {
const parsed = this._parseSSEMessage(buffer);
if (parsed) {
if (parsed.event && parsed.event !== 'message') {
options.onEvent?.(parsed.event, parsed.data, parsed.id);
} else {
options.onMessage?.(parsed.data, parsed.id);
}
}
}
options.onComplete?.(lastEventId);
} catch (error) {
if (error.name !== 'AbortError') {
options.onError?.(error);
}
}
}
/**
* 解析单条 SSE 消息
* @param {string} raw - 原始消息文本
* @returns {{ event?: string, data: string, id?: string, retry?: number } | null}
*/
_parseSSEMessage(raw) {
let event = '';
let data = '';
let id = '';
let retry = null;
for (const line of raw.split('\n')) {
// 忽略注释行(以冒号开头)
if (line.startsWith(':')) continue;
const colonIndex = line.indexOf(':');
if (colonIndex === -1) continue;
const field = line.slice(0, colonIndex);
let value = line.slice(colonIndex + 1);
// 如果值以空格开头,去掉前导空格(SSE 规范)
if (value.startsWith(' ')) value = value.slice(1);
switch (field) {
case 'event':
event = value;
break;
case 'data':
// data 字段可多行,用 \n 连接
data += (data ? '\n' : '') + value;
break;
case 'id':
id = value;
break;
case 'retry':
retry = parseInt(value, 10);
break;
}
}
if (!data && !event) return null;
return { event: event || 'message', data, id, retry };
}
}
使用示例
javascript
const client = new SSEStreamClient({
baseURL: 'https://api.example.com',
headers: {
'Authorization': 'Bearer sk-xxxxx'
}
});
// AI 对话流
await client.connect('/v1/chat/completions', {
model: 'gpt-4',
messages: [
{ role: 'user', content: '解释什么是 SSE' }
],
stream: true
}, {
onMessage(data) {
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content || '';
document.getElementById('output').textContent += content;
} catch {
// 非 JSON 数据,直接显示
document.getElementById('output').textContent += data;
}
},
onEvent(event, data) {
console.log(`[事件: ${event}]`, data);
},
onError(error) {
console.error('SSE 错误:', error);
document.getElementById('output').textContent += '\n\n[连接出错]';
},
onComplete() {
console.log('流式传输完成');
document.getElementById('output').textContent += '\n\n[完成]';
}
});
// 需要中断时
// client.abort();
四、方案对比与选型建议
| 特性 | EventSource | fetch + ReadableStream |
|---|---|---|
| HTTP 方法 | 仅 GET | 任意(GET/POST/PUT 等) |
| 自定义请求头 | ❌ 不支持 | ✅ 完全支持 |
| 请求体 | ❌ 不支持 | ✅ 支持 JSON / FormData 等 |
| 自动重连 | ✅ 内置 | ❌ 需手动实现 |
| Last-Event-ID | ✅ 自动携带 | ❌ 需手动实现 |
| HTTP 状态码 | ❌ 无法获取 | ✅ 可获取 |
| 流解析 | ✅ 浏览器自动 | ❌ 需手动解析 SSE 格式 |
| 浏览器兼容性 | ✅ 好(除 IE) | ✅ 好(需支持 Streams API) |
| 实现复杂度 | ⭐ 极低 | ⭐⭐⭐ 中等 |
| 典型场景 | 通知推送、实时监控 | AI 对话、复杂查询 |
选型决策树:
sql
需要 SSE?
├── 接口是 GET?
│ ├── 无需自定义请求头? ─── ✅ EventSource(最简方案)
│ └── 需要自定义请求头? ─── ⚠️ fetch + ReadableStream
│ (或通过 URL 参数传递 token,但需评估安全风险)
└── 接口是 POST?
└── ✅ fetch + ReadableStream(唯一选择)
五、常见陷阱与最佳实践
5.1 陷阱:忽略 SSE 消息的边界问题
ReadableStream 的 read() 返回的是任意大小的字节块,一个 chunk 可能包含不完整的 SSE 消息,也可能包含多条消息。
javascript
// ❌ 错误:假设每次 read() 恰好返回一条完整消息
const { value } = await reader.read();
const data = JSON.parse(decoder.decode(value)); // 可能抛出解析错误
// ✅ 正确:使用缓冲区拼接,按 \n\n 分割消息
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split('\n\n');
buffer = parts.pop(); // 保留未完成的部分
for (const part of parts) {
// 处理完整消息...
}
}
5.2 陷阱:忘记设置 { stream: true }
TextDecoder.decode() 的第二个参数 stream: true 告诉解码器:数据可能被截断在多字节字符中间,不要将未完成的字节序列替换为替换字符。
javascript
// ❌ 可能导致中文字符丢失
buffer += decoder.decode(value);
// ✅ 正确:保留未完成的字节序列,等待下次拼接
buffer += decoder.decode(value, { stream: true });
5.3 陷阱:EventSource 连接泄漏
创建 EventSource 后忘记关闭,会导致连接堆积。
javascript
// ❌ 错误:组件卸载时未关闭
useEffect(() => {
const es = new EventSource('/api/updates');
es.onmessage = (e) => setState(e.data);
// 忘记 return cleanup!
}, []);
// ✅ 正确:组件卸载时关闭连接
useEffect(() => {
const es = new EventSource('/api/updates');
es.onmessage = (e) => setState(e.data);
return () => es.close(); // 清理!
}, []);
5.4 陷阱:EventSource 的 CORS 问题
EventSource 不支持自定义请求头,因此无法在请求中携带 Authorization 头。如果跨域接口需要鉴权,只能:
- 使用
fetch + ReadableStream方案 - 通过 URL 参数传递 token(不推荐,有安全风险)
- 使用 Cookie 鉴权(需要服务端配合
withCredentials)
javascript
// EventSource 的 withCredentials 选项(仅用于 Cookie)
const es = new EventSource('https://api.other.com/stream', {
withCredentials: true // 发送跨域 Cookie
});
5.5 最佳实践:统一的错误处理
javascript
async function fetchSSE(url, body, callbacks) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
// 处理各种 HTTP 错误状态码
if (response.status === 401) {
callbacks.onAuthError?.();
return;
}
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
callbacks.onRateLimit?.(retryAfter);
return;
}
if (!response.ok) {
callbacks.onHttpError?.(response.status, response.statusText);
return;
}
// 正常读取流...
const reader = response.body.getReader();
// ...
}
5.6 最佳实践:React 中封装自定义 Hook
javascript
import { useEffect, useRef, useCallback, useState } from 'react';
/**
* SSE 流式请求 Hook
* @param {string} url - 请求地址
* @param {object} options - 配置项
*/
function useSSEStream(url, options = {}) {
const [data, setData] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const abortRef = useRef(null);
const send = useCallback(async (body) => {
setLoading(true);
setData('');
setError(null);
const controller = new AbortController();
abortRef.current = controller;
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: JSON.stringify(body),
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const reader = response.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 parts = buffer.split('\n\n');
buffer = parts.pop() || '';
for (const part of parts) {
if (!part.trim()) continue;
for (const line of part.split('\n')) {
if (!line.startsWith('data:')) continue;
const content = line.slice(5).trimStart();
if (content === '[DONE]') continue;
try {
const parsed = JSON.parse(content);
const chunk = parsed.choices?.[0]?.delta?.content || '';
if (chunk) setData(prev => prev + chunk);
} catch {
setData(prev => prev + content);
}
}
}
}
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
abortRef.current = null;
}
}, [url, options.headers]);
const abort = useCallback(() => {
abortRef.current?.abort();
}, []);
// 组件卸载时自动中断
useEffect(() => {
return () => abortRef.current?.abort();
}, []);
return { data, loading, error, send, abort };
}
// 使用
function ChatPanel() {
const { data, loading, error, send, abort } = useSSEStream('/api/chat', {
headers: { 'Authorization': 'Bearer xxx' }
});
return (
<div>
<pre>{data}</pre>
<button onClick={() => send({ prompt: '你好' })} disabled={loading}>
发送
</button>
{loading && <button onClick={abort}>停止生成</button>}
{error && <p className="error">{error}</p>}
</div>
);
}
六、封装一个通用的 SSE 客户端
将两种方案统一封装,根据接口方法自动选择实现:
javascript
/**
* 统一 SSE 客户端
* - GET 请求自动使用 EventSource(享受自动重连)
* - POST 请求使用 fetch + ReadableStream(支持请求体和自定义头)
*/
class UnifiedSSEClient {
constructor(baseURL = '', defaultHeaders = {}) {
this.baseURL = baseURL;
this.defaultHeaders = defaultHeaders;
}
/**
* 发起 SSE 连接
* @param {object} config
* @param {string} config.url - 请求路径
* @param {'GET'|'POST'} config.method - HTTP 方法
* @param {object} config.body - 请求体(POST 时使用)
* @param {object} config.headers - 额外请求头
* @param {function} config.onMessage - 消息回调 (data: string) => void
* @param {function} config.onEvent - 事件回调 (event: string, data: string) => void
* @param {function} config.onError - 错误回调 (error: Error) => void
* @param {function} config.onComplete - 完成回调 () => void
* @returns {function} 取消函数
*/
connect(config) {
const method = (config.method || 'GET').toUpperCase();
if (method === 'GET') {
return this._connectViaEventSource(config);
} else {
this._connectViaFetch(config);
// fetch 方案返回 abort 函数
return () => this._abortController?.abort();
}
}
// ─── GET: EventSource 方案 ───
_connectViaEventSource(config) {
const url = `${this.baseURL}${config.url}`;
const es = new EventSource(url);
es.onmessage = (event) => {
config.onMessage?.(event.data, event.lastEventId);
};
// 监听自定义事件
if (config.onEvent) {
// 注意:addEventListener 需要提前知道事件名
// 通用方案:监听所有事件需要重写 onmessage
const originalOnMessage = es.onmessage;
es.onmessage = null;
es.addEventListener('message', (event) => {
config.onMessage?.(event.data, event.lastEventId);
});
}
es.onerror = (event) => {
if (es.readyState === EventSource.CLOSED) {
config.onComplete?.();
} else {
config.onError?.(new Error('EventSource connection error'));
}
};
// 返回取消函数
return () => es.close();
}
// ─── POST: fetch + ReadableStream 方案 ───
async _connectViaFetch(config) {
this._abortController = new AbortController();
try {
const response = await fetch(`${this.baseURL}${config.url}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.defaultHeaders,
...config.headers
},
body: JSON.stringify(config.body || {}),
signal: this._abortController.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
await this._readStream(response, config);
} catch (error) {
if (error.name !== 'AbortError') {
config.onError?.(error);
}
} finally {
this._abortController = null;
config.onComplete?.();
}
}
async _readStream(response, config) {
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 });
const parts = buffer.split('\n\n');
buffer = parts.pop() || '';
for (const part of parts) {
const parsed = this._parseSSE(part);
if (!parsed) continue;
if (parsed.event === 'message') {
config.onMessage?.(parsed.data, parsed.id);
} else {
config.onEvent?.(parsed.event, parsed.data, parsed.id);
}
}
}
// 处理剩余数据
if (buffer.trim()) {
const parsed = this._parseSSE(buffer);
if (parsed) {
if (parsed.event === 'message') {
config.onMessage?.(parsed.data, parsed.id);
} else {
config.onEvent?.(parsed.event, parsed.data, parsed.id);
}
}
}
}
_parseSSE(raw) {
let event = 'message';
let data = '';
let id = '';
for (const line of raw.split('\n')) {
if (line.startsWith(':')) continue;
const idx = line.indexOf(':');
if (idx === -1) continue;
const field = line.slice(0, idx);
let value = line.slice(idx + 1);
if (value.startsWith(' ')) value = value.slice(1);
if (field === 'event') event = value;
else if (field === 'data') data += (data ? '\n' : '') + value;
else if (field === 'id') id = value;
}
return data ? { event, data, id } : null;
}
}
// ─── 使用示例 ───
const sse = new UnifiedSSEClient('https://api.example.com', {
'Authorization': 'Bearer sk-xxxxx'
});
// GET 方式(自动使用 EventSource)
const disconnect = sse.connect({
url: '/api/notifications',
method: 'GET',
onMessage(data) {
console.log('通知:', data);
}
});
// disconnect(); // 关闭连接
// POST 方式(自动使用 fetch + ReadableStream)
const abort = sse.connect({
url: '/v1/chat/completions',
method: 'POST',
body: {
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }],
stream: true
},
onMessage(data) {
const parsed = JSON.parse(data);
const chunk = parsed.choices?.[0]?.delta?.content || '';
process.stdout.write(chunk);
},
onError(error) {
console.error('出错:', error);
}
});
// abort(); // 中断请求
总结
| 场景 | 推荐方案 | 核心理由 |
|---|---|---|
| GET 接口,无需自定义请求头 | EventSource | 零配置、自动重连、API 简洁 |
| POST 接口,需要请求体/自定义头 | fetch + ReadableStream | 唯一选择,灵活可控 |
| 需要统一管理两种场景 | UnifiedSSEClient | 一套 API 自动适配 |
- SSE 的核心在于理解其协议格式(
data:\n\n),无论用哪种客户端方案,底层都是解析这个格式。 - EventSource 帮你自动解析了,而 fetch 方案需要你手动处理------但也因此获得了完全的控制力。