SSE(Server-Sent Events)
SSE 很适合放在请求与数据层里看,因为它处理的不是普通接口查询,而是“服务端持续往客户端推送数据流”。
如果只需要服务端单向往前端推送消息,而不需要客户端在同一条连接里频繁回发消息,SSE 往往会比 WebSocket 更轻,也更容易接进现有 HTTP 体系。
SSE 在解决什么
普通 HTTP 请求更像这样:
- 客户端发起
- 服务端响应
- 一次请求结束
一旦进入这些场景,就会显得不够顺:
- AI 对话流式输出
- 实时日志
- 任务进度流
- 审核状态流
- 单向通知流
如果这些场景继续用短轮询,常见问题就是:
- 更新不够及时
- 空转请求很多
- 前端要自己不停拉
- 服务端和网关资源会被重复请求放大
SSE 的核心价值,就是把一次 HTTP 响应拉成长连接事件流,让服务端可以持续往前推消息。
先记住它的几个特征
- 基于 HTTP
- 服务端单向推送
- 浏览器原生支持
EventSource - 默认文本协议,内容类型是
text/event-stream - 原生模型自带自动重连
- 很适合流式输出、通知流、日志流
- 不适合高频双向互动
MDN 对 SSE 的描述也很明确:浏览器可以通过 EventSource 接收服务端持续发送的事件流。
它和普通 HTTP 的区别
SSE 不是“反复请求一个接口”,而是:
- 客户端建立一次 HTTP 请求
- 服务端保持响应不断开
- 后续持续写入事件块
- 客户端边收边处理
这意味着:
- 服务端不能等数据全部准备好再一次性返回
- 网关和代理层要允许长时间保持连接
- 前端要按流式思路处理,而不是按一次
await res.json()的思路处理
SSE 的协议格式怎么理解
SSE 用的是纯文本协议。一个事件块通常像这样:
event: message
id: msg_001
data: hello
每个事件以空行结束。常见字段有:
eventdataidretry
event
表示事件类型:
event: progress
data
表示事件内容。可以出现多行。
data: {"step": 1}
id
表示事件 ID,通常配合断线重连和续传使用。
id: chunk_120
retry
表示建议的重连间隔,单位是毫秒。
retry: 3000
注释行
以 : 开头的行通常用来做心跳:
: ping
一个最小服务端输出示例
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, no-transform
Connection: keep-alive
id: 1
event: message
data: hello
id: 2
event: message
data: world
响应头最少要关注什么
最关键的通常是:
Content-Type: text/event-stream; charset=utf-8Cache-Control: no-cache, no-transformConnection: keep-alive
其中 no-transform 很值得加,因为有些中间层会对响应做压缩或改写,流式场景下这会带来额外问题。
原生 EventSource 怎么接
这是比较容易起步的一种方式。
const es = new EventSource('/api/stream', { withCredentials: true })
es.onmessage = (event) => {
console.log(event.data)
}
es.onerror = () => {
console.log('stream error')
}
EventSource 的优点
- 浏览器原生支持
- 自动重连模型直接可用
- API 简单
- 适合快速接流式接口
EventSource 的限制
- 不支持自定义请求头
- 请求方法固定是 GET
- 更适合 Cookie 鉴权或 URL token
- 协议解析相对固定,不像
fetch那么自由
用 fetch 读 SSE 有什么不同
如果需要:
- 自定义请求头
- POST 请求
- 更灵活地处理
ReadableStream - 和现有请求层保持一致
那前端通常会改成 fetch + stream reader。
const res = await fetch('/api/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ prompt: 'hello' }),
})
const reader = res.body?.getReader()
这种方式更灵活,但代价是:
- 需要自己解析 SSE 协议
- 需要自己处理重连
- 需要自己处理心跳和状态恢复
EventSource 和 fetch 怎么选
EventSource 更适合
- GET 即可满足
- Cookie 鉴权即可满足
- 只想快速接稳定的单向流
fetch 更适合
- 需要 POST
- 需要自定义请求头
- 需要和现有请求层统一
- 需要更细的流式控制
一个更接近真实项目的 EventSource 封装
export type SseMessage = {
type: string
data: string
id?: string
}
export class EventSourceClient {
private source: EventSource | null = null
private url = ''
connect(url: string) {
this.url = url
this.source = new EventSource(url, { withCredentials: true })
this.source.onopen = () => {
console.log('sse opened')
}
this.source.onmessage = (event) => {
console.log('sse message', event.data)
}
this.source.onerror = () => {
console.log('sse error')
}
}
close() {
this.source?.close()
this.source = null
}
}