mcpField
W
web-realtime-sse

web-realtime-sse

github.com/agents-inc开发2650 次安装2 天前更新
代码仓库

关于 web-realtime-sse

面向 Web 应用 SSE 实现的开发技能,说明何时选择浏览器管理的 EventSource 或 fetch 流式传输,并涵盖重连、解析、认证和部署注意事项。

主要功能

  1. 01EventSource 生命周期与事件处理
  2. 02基于 fetch 的流解析
  3. 03重连与 Last-Event-ID 恢复
  4. 04凭据和请求头注意事项
  5. 05类型化消息处理
  6. 06代理与 HTTP 部署指南

适用场景

  1. 01逐 token 流式显示生成文本
  2. 02构建通知源和仪表板
  3. 03连接中断后恢复数据流
  4. 04使用授权请求头或 POST 请求体

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-realtime-sse

安装后在 Claude Code 中输入/web-realtime-sse确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
core.md
fetch-streaming.md
reconnection.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>

评论

支持 Markdown