SSE
SSE(Server-Sent Events) 是一种基于 HTTP 的单向推送技术,允许服务端在一个长连接中持续向客户端发送事件。
与 WebSocket 的双向通信不同,SSE 更适合一些只需要从服务器获取数据的场景,比如实时新闻更新、股票行情、通知系统等。
针对项目实际需求(需携带 Authorization 头、支持精细错误处理),评估了三种主流方案。
方案对比
原生浏览器 EventSource API 使用简单,但在鉴权、错误处理及非浏览器环境支持上存在明显限制。
| 特性 | 原生 EventSource |
event-source-polyfill |
@microsoft/fetch-event-source(推荐) |
|---|---|---|---|
| 实现方式 | 浏览器原生 API | 基于 XMLHttpRequest 模拟 EventSource 行为 | 基于 fetch 实现 |
| 请求方法 | 仅 GET,http长连接 |
通常模拟 GET |
基于 fetch,请求控制更灵活 |
| 自定义请求头 | ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| 鉴权方式 | 通常依赖 cookie 或 query | 可传 Authorization |
可传 Authorization |
| 握手阶段校验 | 能力较弱 | 一般 | 可在 onopen 中校验状态码、content-type |
| 错误处理 | 较弱 | 一般 | 细粒度:可区分 HTTP 错误、响应类型异常、流关闭等 |
| 连接控制 | 较弱 | 一般 | 支持 AbortController 便于主动断开 |
| 重试治理 | 不可控,依赖浏览器默认行为 | 一般 | 可结合业务逻辑自定义重试策略 |
| Node / SSR 适配 | 不友好(Node.js 无原生支持) | 主要面向浏览器 | 更容易适配具备 fetch 能力的运行环境 |
| 页面可见性感知 | 无 | 无 | 集成 Page Visibility API 页面隐藏自动断连,可见时恢复(可配置关闭) |
| 适用场景 | 简单 SSE | 兼容场景、旧方案改造 | 现代前端项目、鉴权复杂场景 |
| Github stars | - | 2.1k ⭐ | 2.8k ⭐ |
| github地址 | - | https://github.com/Yaffle/EventSource | https://github.com/Azure/fetch-event-source |
综上,更推荐使用 @microsoft/fetch-event-source
- 基于 fetch 实现,请求控制更灵活,可携带
Authorization等自定义请求头。 - 可在
onopen、onerror、onclose 对连接状态和异常进行更清晰的调试与处理,识别连接中断等问题。 - 支持
AbortController,便于业务侧结合页面生命周期主动中断连接。
相比原生 EventSource 和 event-source-polyfill,它更符合"鉴权能力、连接控制、错误可观测性"的要求
fetch-event-source实践代码

这是一个订阅消息通知的接口示例展示👆
从浏览器调试面板可以看到,SSE 服务端返回的并不是一次性响应,而是持续推送的事件流。
其中,
CONNECT表示连接建立成功,UNREAD_COUNT表示一条具体业务消息,消息体为{"message":0}。前端可以在
onmessage中根据事件类型分别处理连接状态和业务数据,这也是 SSE 特别适合通知、状态流等场景的原因。
javascript
import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source'
class FatalError extends Error {}
class RetriableError extends Error {}
interface NoticeSubscribeOptions {
token: string
appId?: string
onConnected?: () => void
onUnreadCount?: (count: number) => void
onFatalError?: (message: string) => void
}
export function subscribeNoticeSSE(
url: string,
options: NoticeSubscribeOptions,
) {
const controller = new AbortController()
const task = fetchEventSource(url, {
method: 'GET',
signal: controller.signal,
openWhenHidden: true,
headers: {
Accept: 'text/event-stream',
Authorization: options.token.startsWith('Bearer ')
? options.token
: `Bearer ${options.token}`,
...(options.appId ? { appId: options.appId } : {}),
},
async onopen(response) {
if (response.status === 401 || response.status === 403) {
throw new FatalError('SSE 鉴权失败')
}
if (!response.ok) {
throw new RetriableError(`SSE 连接失败,HTTP ${response.status}`)
}
const contentType = response.headers.get('content-type') || ''
if (!contentType.includes('text/event-stream')) {
throw new FatalError(`接口返回的不是 SSE:${contentType}`)
}
},
onmessage(event: EventSourceMessage) {
switch (event.event) {
case 'CONNECT':
options.onConnected?.()
return
case 'UNREAD_COUNT': {
try {
const payload = JSON.parse(event.data) as { message?: number | string }
options.onUnreadCount?.(Number(payload.message ?? 0))
} catch {
throw new FatalError('UNREAD_COUNT 消息格式错误')
}
return
}
default:
console.debug('[SSE] unknown event:', event.event, event.data)
}
},
onclose() {
// 如果业务上不期望服务端主动结束,可以在这里抛错并交给 onerror 重试
throw new RetriableError('SSE 连接已关闭')
},
onerror(error) {
if (error instanceof FatalError) {
options.onFatalError?.(error.message)
throw error
}
console.warn('[SSE retry]', error)
return 3000
},
})
task.catch((error) => {
if (!controller.signal.aborted) {
console.error('[SSE stopped]', error)
}
})
return () => controller.abort()
}