🚀 使用 ReadableStream 优雅地处理 SSE(Server-Sent Events)

在现代 Web 应用中,实时性变得越来越重要。虽然我们有 WebSocket 作为强力工具,但有些场景下,一个轻量、只需要服务器单向推送的方案 ------ Server-Sent Events(SSE) 会是更合适的选择。

而在前端,我们也可以结合 ReadableStreamfor await...of 语法,将流式事件处理写得优雅又现代。

本文将基于以下示例,介绍如何用 ReadableStream 封装 SSE 数据流,并逐步展示它的妙用。


💡 什么是 Server-Sent Events?

SSE 是浏览器内置支持的通信协议,允许服务端通过 HTTP 长连接不断推送消息给前端。前端只需使用原生的 EventSource 对象即可接收这些消息。


✨ 目标:将 EventSource 包装成 ReadableStream

我们希望实现下面这种代码结构:

ts 复制代码
for await (const event of stream) {
  // 处理每一条 SSE 消息
}

这就意味着我们需要把 EventSource 事件包装成一个异步可迭代的流。


🔧 createSSEStream:EventSource + ReadableStream

我们先封装一个 createSSEStream 函数:

ts 复制代码
function createSSEStream(url) {
  return new ReadableStream({
    start(controller) {
      const eventSource = new EventSource(url);

      eventSource.addEventListener("message", (event) => {
        controller.enqueue({ data: event.data, event: "message" });
      });

      eventSource.addEventListener("update", (event) => {
        controller.enqueue({ data: event.data, event: "update" });
      });

      eventSource.addEventListener("ping", (event) => {
        controller.enqueue({ data: event.data, event: "ping" });
      });

      eventSource.addEventListener("close", (event) => {
        controller.enqueue({ data: event.data, event: "close" });
        eventSource.close();
        controller.close(); // 关闭流
      });

      eventSource.onerror = (err) => {
        console.error("SSE Error:", err);
        eventSource.close();
        controller.error(err); // 推送错误
      };
    },
  });
}

这段代码的关键点:

  • ReadableStream 封装了事件监听;
  • 每个事件都会通过 controller.enqueue() 推送数据;
  • 通过监听 close 事件来主动终止连接;
  • 在出错时关闭流并抛出错误。

🧪 使用 async/await 优雅消费流

封装好之后,我们可以像消费文件、网络流那样来使用 SSE:

ts 复制代码
const stream = createSSEStream("/sse");

for await (const chunk of stream) {
  console.log("接收到 SSE 事件:", chunk.event, chunk.data);
}

这让我们的事件处理逻辑可以完全异步串行写,无需陷入多个嵌套的回调地狱。


🎨 给每类事件加点视觉效果

假设我们要把每类事件都显示到页面上,可以这么写:

ts 复制代码
const backgroundColors = {
  message: "linear-gradient(135deg, #6a11cb, #2575fc)",
  update: "linear-gradient(135deg, #ff7e5f, #feb47b)",
  ping: "linear-gradient(135deg, #43cea2, #185a9d)",
  close: "linear-gradient(135deg, #ff512f, #dd2476)",
};

const outer = document.querySelector("#outer");

for await (const x of stream) {
  const dom = document.createElement("div");
  dom.innerHTML = `
    <div class='item-wrapper' style="
      margin-bottom: 10px; 
      padding: 10px; 
      border-radius: 5px; 
      background: ${backgroundColors[x.event] || backgroundColors.message}; 
      color: white;
      font-family: Arial;
      transform: scale(0.9);
      transition: all .3s ease;
    ">
      <strong>Event:</strong> ${x.event}<br>
      <strong>Data:</strong> ${x.data}
    </div>
  `;
  outer.appendChild(dom);
  setTimeout(() => {
    dom.firstElementChild.style.transform = "scale(1)";
  }, 100);
}

你可以结合 JSON.stringify 来格式化数据展示,或者为每种事件设计不同的 UI。


🧱 小结

  • ReadableStream 让我们可以像处理 async 数据源一样处理 SSE;
  • for await...of 语法写起来非常顺滑,易于维护;
  • SSE 在适合"服务端单向推送"的场景(如实时通知、监控系统)非常合适。

✅ 最后:完整代码仓库

你可以在 GitHub 仓库地址 找到完整示例代码,包含后端 Express 服务和前端 HTML 页面。

相关推荐
Moshow郑锴14 分钟前
Spring Boot 3 + Undertow 服务器优化配置
服务器·spring boot·后端
几何心凉15 分钟前
如何使用 React Hooks 替代类组件的生命周期方法?
前端·javascript·react.js
小堃学编程22 分钟前
前端学习(1)—— 使用HTML编写一个简单的个人简历展示页面
前端·javascript·html
Chandler241 小时前
Go语言即时通讯系统 开发日志day1
开发语言·后端·golang
有梦想的攻城狮1 小时前
spring中的@Lazy注解详解
java·后端·spring
hnlucky1 小时前
通俗易懂版知识点:Keepalived + LVS + Web + NFS 高可用集群到底是干什么的?
linux·前端·学习·github·web·可用性测试·lvs
懒羊羊我小弟2 小时前
使用 ECharts GL 实现交互式 3D 饼图:技术解析与实践
前端·vue.js·3d·前端框架·echarts
前端小巷子2 小时前
CSS3 遮罩
前端·css·面试·css3
运维@小兵2 小时前
vue访问后端接口,实现用户注册
前端·javascript·vue.js
雨汨2 小时前
web:InfiniteScroll 无限滚动
前端·javascript·vue.js