关于 web-realtime-sse
面向 Web 应用 SSE 实现的开发技能,说明何时选择浏览器管理的 EventSource 或 fetch 流式传输,并涵盖重连、解析、认证和部署注意事项。
主要功能
- 01EventSource 生命周期与事件处理
- 02基于 fetch 的流解析
- 03重连与 Last-Event-ID 恢复
- 04凭据和请求头注意事项
- 05类型化消息处理
- 06代理与 HTTP 部署指南
适用场景
- 01逐 token 流式显示生成文本
- 02构建通知源和仪表板
- 03连接中断后恢复数据流
- 04使用授权请求头或 POST 请求体
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-realtime-sse安装后在 Claude Code 中输入/web-realtime-sse确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/core.mdfetch-streaming.mdreconnection.md详细介绍
服务器发送事件(SSE)模式
快速指南: SSE 通过普通 HTTP 响应将文本从服务器推送到客户端,因此可以穿过会阻止更复杂通信方式的代理和防火墙。
EventSource免费提供重连和Last-Event-ID重放功能,但它仅支持 GET,且无法设置标头;fetch 流式传输则放弃这两项功能,换取自定义标头、POST 请求体和AbortController。以下事实会影响选择:EventSource会在网络错误时重试,但遇到 HTTP 错误状态码时会永久放弃;retry:的单位是毫秒;HTTP/2 及更高版本禁止使用Connection: keep-alive。
详细资源:
- examples/core.md — EventSource 生命周期、命名事件、凭据、状态跟踪封装器、类型化消息,以及基于这些内容构建的 React 钩子
- examples/fetch-streaming.md — 带缓冲区处理的流读取器、字段解析器、身份验证标头、POST 流式传输、逐令牌 UI
- examples/reconnection.md —
Last-Event-ID恢复、指数退避、运行状况检查、根据页面可见状态暂停 - reference.md — 消息格式、字段行为、readyState 值、必需的响应标头、EventSource 行为表
适用方式
EventSource— 浏览器会自动重连、通过Last-Event-ID重放,并为你解析线路格式。它仅发送 GET 请求,不设置标头,并通过 Cookie 进行身份验证(withCredentials: true)。请从 examples/core.md 开始。- Fetch 流式传输 — 如果流需要
Authorization标头、POST 请求体或由你控制的取消操作,请使用此方式。此时,你需要负责重连、退避、Last-Event-ID和字段解析。请参阅 examples/fetch-streaming.md。
两种方式都使用相同的线路格式,因此它们共享解析器和消息类型。
<critical_requirements>
编写 SSE 代码前
在消费者离开时调用 eventSource.close()。 打开的流会占用浏览器的域名连接数上限,还会持续向无人监听的处理程序推送数据。
在 onerror 中根据 readyState 分支处理。 CONNECTING 表示浏览器已在重试,正确做法是等待;CLOSED 表示浏览器已放弃,是否重新连接由你决定。
服务器发出的每条消息都要包含 id:。 浏览器会在下次连接时通过 Last-Event-ID 返回最后一个 ID,这样服务器就能从断点续传,而不是从头开始。
响应中设置 Content-Type: text/event-stream 和 Cache-Control: no-cache。 不要设置 Connection: keep-alive——HTTP/2 及更高版本禁止此标头,而且 Safari 会拒绝携带它的响应。
定期发送注释行(: keep-alive)。 代理会关闭它们认为处于空闲状态的流,通常在 60–120 秒后关闭;注释行不会传到任何处理程序,但能重置计时。
</critical_requirements>
自动检测: EventSource、text/event-stream、Last-Event-ID、eventSource.onmessage、eventSource.readyState、EventSource.CONNECTING、withCredentials、addEventListener("message")、retry:、data:、event:、id:、ReadableStream、TextDecoder、response.body.getReader
适用于:
- 通过普通 HTTP 从服务器向客户端推送数据——通知、信息流、仪表板
- 逐个令牌流式传输生成的文本
- 客户端只监听的实时数据流
- 通过
Last-Event-ID实现可恢复的流 - 无法使用
EventSource时,手动解析 SSE 线路格式
其他部分会处理:
- 频繁的客户端到服务器消息——SSE 不提供上行通道,因此需要上行通道的客户端要么将流与普通请求配合使用,要么选择双向传输方式。
- 二进制载荷——线路格式使用 UTF-8 文本;二进制数据必须编码,体积会因此增加约三分之一。
- 服务器自身的流实现及其重放存储。
- 消息接收后的存储位置及其呈现方式。
- 签发并刷新用于验证流身份的令牌。
SSE 是一个永不结束的 HTTP 响应。这就是它的全部设计理念,一切特性都由此而来:它可以通过现有的 HTTP 基础设施工作,线路格式可读,而且由于无需重新握手,浏览器可以负责重新连接。
- 由浏览器重新连接,而不是由你来连接——
EventSource会按自己的计划重试,服务器可通过retry:调整该计划。 - 通过标头实现重放——服务器会看到
Last-Event-ID,并决定要重新发送什么。 - 格式只有五种字段——
data:、event:、id:、retry:和单独的:注释。
CONNECTING (0) → OPEN (1) → messages… → CLOSED (2)
↓ ↓
(error) ← auto-reconnect ← (connection lost)
<decision_framework>
验证流的身份
对于同源请求或携带凭据的跨源请求,EventSource 只支持 Cookie 这一种机制——设置 withCredentials: true,并让服务器在 CORS 中允许凭据。Bearer 令牌需要使用 fetch 流式传输,因为令牌应放在 Authorization 标头中,而不是 URL 中。短期令牌还需要在重新连接时获取新令牌,这也是这种情况适合使用 fetch 流式传输的另一个原因。
在基础设施后部署
在 HTTP/1.1 上,一个流会占用每个域名大约六个连接中的一个,因此多个并发流会耗尽连接,影响页面其余部分;HTTP/2 会对连接进行多路复用,从而消除这一上限。反向代理默认会缓冲响应,并在缓冲区填满前一直扣留消息——请为该路由关闭缓冲(nginx 使用 X-Accel-Buffering: no),并通过 Cache-Control: no-transform 避免转换。在无服务器平台上,依赖长连接流之前,请先确认响应超时设置。
</decision_framework>
核心模式
模式 1:EventSource 生命周期
三个处理程序覆盖了全部场景,而 onerror 中的 readyState 可以区分正在重试和已断开的流。
const eventSource = new EventSource(SSE_URL);
eventSource.onopen = () => setStatus("open");
eventSource.onmessage = (event: MessageEvent) =>
handle(event.data, event.lastEventId);
eventSource.onerror = () => {
if (eventSource.readyState === EventSource.CLOSED) reconnectManually();
};
完整代码:examples/core.md
模式 2:命名事件类型
带有 event: 字段的消息会发送给同名监听器,而不是 onmessage。
eventSource.addEventListener("notification", (event: MessageEvent) => {
show(JSON.parse(event.data));
});
// messages with no event: field still arrive here
eventSource.onmessage = (event: MessageEvent) => handleDefault(event.data);
完整代码:examples/core.md
模式 3:凭据与跨源请求
withCredentials 会将 Cookie 发送到另一个源;CORS 配置错误会通过 onerror 暴露出来,但不会提供更具体的信息。
const eventSource = new EventSource(SSE_URL, { withCredentials: true });
完整代码:examples/core.md
模式 4:连接状态与手动重试
EventSource 会自动重试网络故障,但遇到 HTTP 错误状态码时会永久停止。跟踪状态可以让 UI 有内容可显示,也为这类情况提供重试入口。
eventSource.onerror = () => {
if (eventSource.readyState === EventSource.CLOSED) {
setStatus("closed");
scheduleRetry(); // the browser will not do this one
} else {
setStatus("error"); // CONNECTING — the browser is already on it
}
};
完整代码:examples/core.md
模式 5:SSE 消息格式
字段以 \n 分隔,消息以 \n\n 结束。共有五种字段类型:data: 负载、event: 名称、id: 恢复点、retry: 以毫秒为单位的重连间隔,以及单独的 : 注释。
event: notification
data: {"title": "New message"}
id: msg-002
: keep-alive comment (never delivered to a handler)
多行 data: 会以 \n 拼接;id: 会持续生效,直到后续消息更改它;retry: 会用于之后的每次重连。
完整字段与行为表:reference.md
模式 6:类型化消息处理
基于负载使用可辨识联合类型,可以让 switch 覆盖所有情况。因此,新增服务器消息类型会导致编译错误,而不是被某个分支静默忽略。
type SSEMessage =
| { type: "notification"; title: string; body: string }
| { type: "user-update"; userId: string; action: "joined" | "left" }
| { type: "heartbeat"; serverTime: number };
function handle(message: SSEMessage): void {
switch (message.type) {
case "notification":
return show(message.title, message.body);
case "user-update":
return updatePresence(message.userId, message.action);
case "heartbeat":
return updateServerTime(message.serverTime);
default: {
const exhaustive: never = message;
return exhaustive;
}
}
}
完整代码:examples/core.md
<red_flags>
注意事项
运行时会出问题:
- 消费者卸载时没有调用
close()——流会保持打开状态,占用每个域名的连接数限制,并持续向已失效的处理程序发送消息。 - 创建新的
EventSource时没有关闭之前的实例——两个实例都会保持运行,每条消息都会收到两次。 - 未处理
onerror——无法区分流已失败还是暂时没有消息,UI 会无限期显示过时数据。 - 对
event.data调用JSON.parse时没有使用try——一条格式错误的消息就会导致后续所有消息的处理程序失效。 - 将令牌放在 URL 查询字符串中——服务器会记录该令牌,浏览历史会保留该令牌,代理也能看到该令牌。应使用 Cookie,或通过带有
Authorization标头的流式请求获取数据。 - 需要 POST 时使用
EventSource——它只会发起 GET 请求。 - 将 Fetch 流中的每个数据块都当作一条完整消息处理——数据块边界可能落在消息中间,因此应缓冲数据并按
\n\n分割。 - 使用
TextDecoder时没有传入{ stream: true }——跨数据块拆分的多字节字符会被解码成乱码。 - 未验证消息内容就直接渲染——负载中的文本可能受攻击者控制,而类型化接口只是在编译时作出保证,并不能保证运行时安全。
容易引起意外的行为:
EventSource没有超时机制——网络层面已经断开的连接可能仍保持OPEN数分钟,之后才触发onerror。保持连接的注释和客户端健康检查就是为了发现这种情况。- 它会重试网络错误,但会将 HTTP 4xx 或 5xx 视为最终错误。因此,最可能需要重试的情况恰恰是它不会重试的情况。
retry:的单位是毫秒。服务器发送retry: 5时,每隔 5ms 就会重连一次。data:\n\n会传递一个空字符串,而不是不传递任何内容——通过真假值检查会将真实消息误判为不存在。- 多行负载应写成多行
data:,而不是在一行中使用转义换行符。 - 空的
id:会清除Last-Event-ID,而不是保留之前的值。 - 重连时流会恢复,但组件状态不会自行重置,因此需要将断连前累积的内容与重放的数据进行协调。
</red_flags>