WebSockets

H3 内置跨平台 WebSocket 和服务器发送事件的实用工具。

您可以使用 🔌 CrossWS 为 H3 服务器添加跨平台 WebSocket 支持。

#用法

WebSocket 处理程序可以使用 defineWebSocketHandler() 工具定义,并像事件处理程序一样注册到任意路由。

你需要在 serve 函数中将 CrossWS 注册为服务器插件。该插件会自动从你匹配到的路由中解析出正确的 hooks。

import { H3, serve, defineWebSocketHandler } from "h3";

import { plugin as ws } from "crossws/server";

const app = new H3();

app.get("/_ws", defineWebSocketHandler({ message: console.log }));

serve(app, {
  plugins: [ws()],
});

Note

只有在你需要自己解析 hooks(例如不调用应用)时,才需要向 ws() 传入自定义 resolve。默认情况下,CrossWS 会调用应用的 fetch 处理程序,并读取由 defineWebSocketHandler() 附加的 hooks。

defineWebSocketHandler() 会将 hooks 附加到 请求 上(CrossWS 使用 getWebSocketHooks(request) 将它们读取回来,键为 Symbol.for("crossws.hooks")),并为任何不是 WebSocket 客户端的请求以 426 Upgrade Required 响应升级请求。

之所以将它们附加到请求而不是响应上,是因为 Response 在离开应用的过程中会被重新构建——例如合并由路由规则或 CORS 中间件暂存的标头,或者由任何执行 new Response(res.body, res) 的中间件重新构建——而重新构建的响应不包含原始响应的任何自有属性。响应也会将 hooks 作为 res.crossws 暴露出来以方便使用,但只有在没有任何内容重新构建它时才会如此;如果你编写了自定义 resolve,请改为读取请求:

import { getWebSocketHooks } from "crossws";

serve(app, {
  plugins: [ws({ resolve: (req) => app.fetch(req).then(() => getWebSocketHooks(req)) })],
});

完整示例:

websocket.mjs
import { H3, serve, html, defineWebSocketHandler } from "h3";
import { plugin as ws } from "crossws/server";

export const app = new H3();

// 一个用于普通 HTTP 请求的最小自包含 WebSocket 演示页。
const playground = html`<!doctype html>
  <title>H3 WebSocket 演示页</title>
  <h1>H3 WebSocket 演示页</h1>
  <form id="form">
    <input id="input" placeholder="输入消息..." autocomplete="off" autofocus />
    <button type="submit">发送</button>
  </form>
  <div id="log"></div>
  <script type="module">
    const log = (msg) => {
      const line = document.createElement("div");
      line.textContent = msg;
      document.getElementById("log").append(line);
    };
    const url = location.href.replace(/^http/, "ws");
    const ws = new WebSocket(url);
    ws.addEventListener("open", () => log("[open] 已连接到 " + url));
    ws.addEventListener("message", (e) => log("[message] " + e.data));
    ws.addEventListener("close", () => log("[close] 已断开连接"));
    document.getElementById("form").addEventListener("submit", (e) => {
      e.preventDefault();
      const input = document.getElementById("input");
      ws.send(input.value);
      input.value = "";
    });
  </script>`;

// 单一路由同时提供演示页(普通 HTTP)和
// WebSocket 端点(升级请求)。页面会连接回自身。
app.get(
  "/",
  defineWebSocketHandler(
    {
      open(peer) {
        console.log("[open]", peer);

        // 向新客户端发送欢迎信息
        peer.send("欢迎来到服务器!");

        // 将新客户端加入 "chat" 频道
        peer.subscribe("chat");

        // 通知其他所有已连接客户端
        peer.publish("chat", `[系统] ${peer} 已加入!`);
      },

      message(peer, message) {
        console.log("[message]", peer);

        if (message.text() === "ping") {
          // 向客户端回复一个 ping 响应
          peer.send("pong");
          return;
        }

        // 服务器会将收到的消息重新广播给所有人
        peer.publish("chat", `[${peer}] ${message}`);

        // 将消息回显给发送者
        peer.send(message);
      },

      close(peer) {
        console.log("[close]", peer);
        peer.publish("chat", `[系统] ${peer} 已离开聊天室!`);
        peer.unsubscribe("chat");
      },
    },
    // 非升级请求将返回演示页。
    () => playground,
  ),
);

serve(app, {
  plugins: [ws()],
});

#处理 HTTP 请求

默认情况下,WebSocket 路由会对任何非 WebSocket 升级请求返回 426 Upgrade Required。

你可以向 defineWebSocketHandler() 传入一个可选的 HTTP 处理器作为第二个参数,以在同一路由上处理普通(非升级)请求。WebSocket 升级请求仍然会进入这些 hooks。

app.get(
  "/_ws",
  defineWebSocketHandler({ message: (peer, message) => peer.send(message.text()) }, () => "发送 WebSocket 升级请求以连接。"),
);

#服务器发送事件(SSE)

作为 WebSockets 的替代方案,您可以使用服务器发送事件。

H3 内置了 EventStream 类,用于创建服务器发送事件。直接使用 new EventStream(event) 构造它,并从处理程序中返回。

#示例

server-sent-events.mjs
import { H3, serve, EventStream } from "h3";

export const app = new H3();

app.get("/", (event) => {
  const eventStream = new EventStream(event);

  // 每秒发送一条消息
  const interval = setInterval(async () => {
    await eventStream.push("Hello world");
  }, 1000);

  // 当连接关闭或写入器关闭时清理定时器
  eventStream.onClosed(() => {
    console.log("连接已关闭");
    clearInterval(interval);
  });

  return eventStream;
});

serve(app);