前端SSE主流实现方案

1 阅读7分钟

最近在工作中,接触到SSE比较多,从实时推送的需求谈起 ,在目前Web实时通信领域,比较常用的两个技术就是 SSEwebSocket

对于服务器向客户端单向推送数据, 如 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的对比选择

特性SSEWebSocket
通信方向单向(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追踪、极轻量

  1. 自动重连管理:拥有最稳定的重连机制。连接由于网络波动断开后,浏览器会自动根据服务器下发的 retry时间进行重试,无需前端编写重连代码。
  2. 内置 ID 追踪 (Last-Event-ID) : 服务端发送 id 字段,浏览器底层会自动记录。重连时,浏览器会自动在 HTTP 请求头中带上 Last-Event-ID,确保数据流的连续性。
  3. 极轻量: 无需引入任何第三方库,对打包体积零影响。

2.3 局限性分析

  1. 只支持 GET 请求:无法发送复杂的 JSON 参数。如果你的需求是“发送一段长文本让 AI 总结”,URL 参数长度限制将成为瓶颈。

为什么它只支持 GET 请求?
设计初衷:GET的语义在HTTP规范中是用于“获取”资源。SSE本质上是服务器向客户端推送信息。如果支持 POST 请求,当连接断开触发自动重连时,浏览器必须重新发送 POST 的 Body 数据。这会带来一系列复杂问题:比如数据是否需要重新计算?如果 Body 很大,重连的开销会激增。为了保持“自动重连”这一核心特性的极致简单,标准委员会选择了只支持不带 Body 的 GET

  1. 无法自定义请求头 (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失效问题?
原生 EventSourceonerror 回调函数无法直接获取 HTTP 状态码(如 401)
解决方案:
后端在检测到 Token 过期时,发送一个特殊的 SSE 事件(如 token_expired)然后再断开

  1. 连接控制力弱: 无法修改重连策略。

三、fetch-event-source (现代方案)

现代 Web 开发的首选。基于 fetch API 和 ReadableStream 对 SSE 协议的重新实现

3.1 核心优势

支持 POST 请求:可以发送复杂的 JSON Payload(AI 问答必备)。

可控的 Headers:轻松集成 JWT、自定义权限字段。

生命周期拦截: 提供 onopenonmessageoncloseonerror 完整钩子

onopen:校验响应状态,如:response.status为401(token过期)熔断操作 onmessage:接收数据,收到由 \n\n 结尾的完整数据块时,触发该钩子执行 onclose:服务端正常关闭连接 onerror:连接发生网络异常、服务器崩溃、或手动 throw 错误时,进入此钩子。

自定义重连策略:可以根据错误类型(如 401 停止重试,500 自动指数退避重试)精确控制。

eventSource的重连像一个“盲目的执着者”,无论遇到什么错误都会不断重试。 fetch-event-source 通过 onopenonerror 配合,像编写业务逻辑一样控制重连行为。
指数退避重试:当发生网络抖动且没有手动 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的源码和生产避坑进行分享~