最近在工作中,接触到SSE比较多,从实时推送的需求谈起 ,在目前Web实时通信领域,比较常用的两个技术就是 SSE 和 webSocket。
对于服务器向客户端单向推送数据, 如 AI 文本生成的场景,SSE 以其基于 HTTP 协议、轻量化、自带重连机制的特性,成为了目前开发者最青睐的选择。
本文将深入对比浏览器原生 EventSource 与流行的 fetch-event-source 库
一、SSE 技术概览
1.1 什么是 SSE?
SSE 是一种允许服务器异步地向浏览器推送实时数据的技术。
与传统短链接的“请求-响应”模式不同,SSE 建立的是一种长连接、单向的数据流。
核心秘密:HTTP 响应头中的 Content-Type: text/event-stream
传统的 HTTP 请求中,服务器发送完数据后会关闭连接。
在 SSE 中,服务器会保持连接打开,并利用 HTTP 流(Streaming) 特性。当服务器有新数据时,它会通过这个已经打开的通道发送一个数据块,不需要重新建立 TCP 连接。
1.2 报文详解
SSE 协议基于纯文本,每条消息由一个或多个字段行组成,字段名与值之间用冒号 : 分隔,每行以 \n 结尾。
一个消息块必须以 \n\n 结尾。 如果服务端只发了 data: hello\n 而没有补上第二个 \n,前端将永远无法触发回调函数,因为浏览器认为该消息尚未传输完成。
以冒号
:开头的行将被浏览器忽略,常用于保持长连接(心跳) 。不会触发前端的
onmessage逻辑,减少了 JavaScript 引擎的负载,同时又维持了 TCP 链路的活跃。
标准HTTP响应头:
Content-Type: text/event-stream;
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no # 关键:防止 Nginx 缓存流式数据
标准报文体示例:
: 这是一行注释,用于连接保活(心跳)
: server-time: 1768406400
event: connected
data: {"status":"ready","sessionId":"sess_98765"}
id: 100
retry: 5000
event: progress
data: {"taskId":"doc-2562","step":"parsing"}
data: {"percent":45}
id: 101
: 每隔15秒发送一次心跳
event: heartbeat
data: ping
id: 101
event: progress
data: {"taskId":"doc-2562","step":"completed"}
data: {"percent":100}
id: 102
event: financial_statement_status
data: {"docId":2563,"status":"success"}
id: 103
| 字段名 | 说明与规范含义 | 备注 |
|---|---|---|
data | 消息内容。如果消息跨行,可以连续发送多个 data 行,浏览器会自动将其拼接并以 \n 连接。 | |
event | 事件类型。用于区分不同的消息频道。前端可用 addEventListener('类型', ...) 监听。若省略,默认为 message 事件。 | |
id | 事件唯一标识。浏览器会将其存入“最后一次事件 ID”缓冲区。重连时,该值会放入请求头的 Last-Event-ID。 | 如果一条消息没有 id 字段,浏览器会保持上一个消息的 ID 不变,除非显式清空 |
retry | 重连时间间隔(毫秒)。告诉浏览器如果连接断开,等待多久后再尝试自动重连。 | 大多数浏览器默认为 3000ms(3秒)。 |
1.3 与 WebSocket的对比选择
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(Server -> Client) | 全双工(双向) |
| 协议 | 标准 HTTP | 独立 TCP 协议 |
| 连接开销 | 极低(长连接) | 低(建立连接后开销极小) |
| 延迟 | 低 | 极低 |
| 自动重连 | 原生支持 | 需手动实现 |
| 浏览器限额 | HTTP/1.1 下同域名限 6 个 | 限制较少(通常 50-255) |
选择 SSE 的场景:
1、AI 聊天流:如 ChatGPT 的打字机效果。
2、实时监控面板:股票价格、服务器负载、进度条更新。
3、社交媒体通知:站内消息提醒、点赞通知。
选择 WebSocket 的场景:
1、即时通讯( IM ) :如微信、钉钉,需要双向高频交互。
2、协同办公:多人实时编辑同一个文档。
3、实时竞技游戏:对毫秒级双向延迟有极致要求。
二、原生 EventSource (标准方案)
浏览器原生内置的 API,专门用于接收 text/event-stream 格式的服务器推送。特点是“简单”与“自动化”。
2.1 快速上手:
基础代码实现
// 1. 建立连接(仅支持 GET 请求)
const sse = new EventSource('/api/stream', {
withCredentials: true // 默认为 false
});
// 2. 监听默认消息 (服务器未返回 event 字段的消息)
source.onmessage = (event) => {
console.log('收到新消息:', event.data);
};
// 3. 监听自定义事件 (如服务端发送 event: progress)
source.addEventListener('progress', (event) => {
const data = JSON.parse(event.data);
console.log('任务进度:', data.percent);
});
// 4. 错误处理与重连状态
source.onerror = (error) => {
if (source.readyState === EventSource.CLOSED) {
console.log('连接已关闭');
}
};
2.2 核心优势:自动重连、内置ID追踪、极轻量
- 自动重连管理:拥有最稳定的重连机制。连接由于网络波动断开后,浏览器会自动根据服务器下发的
retry时间进行重试,无需前端编写重连代码。 - 内置 ID 追踪 (Last-Event-ID) : 服务端发送
id字段,浏览器底层会自动记录。重连时,浏览器会自动在 HTTP 请求头中带上Last-Event-ID,确保数据流的连续性。 - 极轻量: 无需引入任何第三方库,对打包体积零影响。
2.3 局限性分析:
- 只支持 GET 请求:无法发送复杂的 JSON 参数。如果你的需求是“发送一段长文本让 AI 总结”,URL 参数长度限制将成为瓶颈。
为什么它只支持 GET 请求?
设计初衷:GET的语义在HTTP规范中是用于“获取”资源。SSE本质上是服务器向客户端推送信息。如果支持 POST 请求,当连接断开触发自动重连时,浏览器必须重新发送 POST 的 Body 数据。这会带来一系列复杂问题:比如数据是否需要重新计算?如果 Body 很大,重连的开销会激增。为了保持“自动重连”这一核心特性的极致简单,标准委员会选择了只支持不带 Body 的 GET
- 无法自定义请求头 (Headers) : 这是最核心的痛点。无法在连接时带上
Authorization: Bearer <Token>
常用token方案:
1、将 Token 拼接在 URL 后(不推荐)。const source = new EventSource('/api/sse?token=${token}');
2、Cookie 鉴权(HttpOnly)。const source = new EventSource('/api/sse', { withCredentials: true });
避坑:怎么解决token失效问题?
原生EventSource的onerror回调函数无法直接获取 HTTP 状态码(如 401)
解决方案:
后端在检测到 Token 过期时,发送一个特殊的 SSE 事件(如token_expired)然后再断开
- 连接控制力弱: 无法修改重连策略。
三、fetch-event-source (现代方案)
现代 Web 开发的首选。基于 fetch API 和 ReadableStream 对 SSE 协议的重新实现
3.1 核心优势
支持 POST 请求:可以发送复杂的 JSON Payload(AI 问答必备)。
可控的 Headers:轻松集成 JWT、自定义权限字段。
生命周期拦截: 提供 onopen、onmessage、onclose、onerror 完整钩子
onopen:校验响应状态,如:response.status为401(token过期)熔断操作onmessage:接收数据,收到由\n\n结尾的完整数据块时,触发该钩子执行onclose:服务端正常关闭连接onerror:连接发生网络异常、服务器崩溃、或手动throw错误时,进入此钩子。
自定义重连策略:可以根据错误类型(如 401 停止重试,500 自动指数退避重试)精确控制。
eventSource的重连像一个“盲目的执着者”,无论遇到什么错误都会不断重试。
fetch-event-source通过onopen和onerror配合,像编写业务逻辑一样控制重连行为。
指数退避重试:当发生网络抖动且没有手动throw错误时,fetch-event-source会默认开启指数退避,重连间隔会随着失败次数增加而拉长(例如:1s -> 2s -> 4s -> 8s...),直到达到一个上限。
3.2 常用配置:
signal:这是最重要的配置项,用于从外部手动强制切断 SSE 连接。
const ctrl = new AbortController();
fetchEventSource('/api/ai', { signal: ctrl.signal, ... });
ctrl.abort(); // 停止连接
retry:设置第一次重连之前的等待时间(毫秒),如果后端在消息流中下发了 retry: 5000,后端的值会覆盖此处的配置
前端主动调用
AbortController.abort(),不会触发onclose,而是直接进入中止状态。
openWhenHidden:页面可见性 API的自动断连与恢复
fetchEventSource('/api/sse', {
// 切换标签页或最小化浏览器时,它会自动断开连接并销毁 AbortController
// 重新切回页面,用之前存储的 Last-Event-ID 自动发起重连
openWhenHidden: false,
});
小结:
项目中还是更推荐fetchEventSource去进行开发的,后面也会继续对fetchEventSource的源码和生产避坑进行分享~