
Nitro WebSocket 实战基于 defineWebSocketHandler 与 Pub/Sub 构建实时聊天室【免费下载链接】nitroNext Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.项目地址: https://gitcode.com/GitHub_Trending/ni/nitroNitro 提供了跨平台的 WebSocket 支持底层由 CrossWS 与 H3 驱动本示例examples/websocket实现了一个使用 WebSocket 的实时聊天室客户端连接、发送消息并实时收到其他用户的消息服务端通过 pub/sub 频道向所有已连接客户端广播消息。读完本文你将掌握defineWebSocketHandler的完整生命周期钩子open/message/close等、peer对象与message对象的用法以及如何用主题topic与命名空间namespace构建可隔离的多人实时通信能力并了解它与 Server-Sent EventsSSE的选型边界。快速运行聊天室示例示例仓库位于 examples/websocket包含四个关键文件文件作用routes/_ws.tsWebSocket 服务端处理逻辑聊天室核心index.html浏览器端聊天界面与客户端连接逻辑nitro.config.tsNitro 配置开启 WebSocket 特性package.jsonnitro dev与nitro build脚本在 examples/websocket 目录下安装依赖后即可启动npm install npm run dev其中npm run dev对应nitro devnpm run build对应nitro build见 package.json。启动后浏览器打开示例自带的 index.html即可看到跨标签页实时同步的聊天室页面——页面会通过new WebSocket(url)自动连接/_ws并内置了发送消息、发送ping、重连、清空聊天记录等交互按钮。开启 WebSocket 特性WebSocket 属于 Nitro 的可选特性需要显式开启。示例的 nitro.config.ts 配置如下import { defineConfig } from nitro; export default defineConfig({ serverDir: ./, renderer: { static: true }, features: { websocket: true }, });核心是features.websocket: true。该字段在 src/types/config.ts 中被声明为可选的websocket?: boolean顶层还有一个被标记为deprecated的旧字段websocket?: boolean官方文档建议统一使用features.websocket。启用后Nitro 会在构建产物中加入 WebSocket 适配层支持所有主流部署目标Node.js、Bun、Deno、Vercel、Cloudflare Workers 等。WebSocket 处理器defineWebSocketHandler创建 WebSocket 路由需要使用defineWebSocketHandler并将其作为默认导出放在路由文件中。WebSocket 处理器与普通请求处理器遵循相同的文件路由机制文件即路由routes/_ws.ts对应/_ws路径以_开头的路由段是路由元信息不进入 URL 匹配。你也可以使用任意路由路径例如routes/chat.ts即可在/chat上处理 WebSocket 连接。defineWebSocketHandler在 Nitro 运行时中直接由 H3 转发导出见 src/runtime/nitro.ts核心运行时通过Symbol.for(crossws.hooks)注册表将 WebSocket 钩子从普通请求中识别出来见 src/runtime/internal/app.ts因此它可以与普通 HTTP 路由、中间件完全共存于同一套文件路由体系中。聊天室完整实现下面这段代码是示例 routes/_ws.ts 的完整内容也是本篇的核心骨架请对照其逐段理解import { defineWebSocketHandler } from nitro; export default defineWebSocketHandler({ open(peer) { peer.send({ user: server, message: Welcome ${peer}! }); peer.publish(chat, { user: server, message: ${peer} joined! }); peer.subscribe(chat); }, message(peer, message) { if (message.text().includes(ping)) { peer.send({ user: server, message: pong }); } else { const msg { user: peer.toString(), message: message.toString(), }; peer.send(msg); // echo peer.publish(chat, msg); } }, close(peer) { peer.publish(chat, { user: server, message: ${peer} left! }); }, });这段处理器覆盖了 WebSocket 生命周期的三个核心钩子其数据流如下open(peer)新连接建立后立即执行。先向该连接自身发送欢迎消息peer.send再向所有已订阅chat主题的在线用户广播“某某加入了”peer.publish最后让该 peer 订阅chat主题peer.subscribe(chat)此后它才能收到其他用户广播的消息。message(peer, message)收到消息时执行。示例实现了两个行为当消息文本包含ping时直接回送pong可用来探测连接存活否则构造包含发送者标识与消息内容的msg对象先用peer.send(msg)回显给发送者本人再用peer.publish(chat, msg)广播给频道内其他所有用户。close(peer)连接关闭时执行向频道广播“某某离开了”让其他在线用户实时感知成员变化。示例的客户端 index.html 对消息协议做了配套处理open后服务端会发送{user:server,message:...}形式的 JSON页面通过JSON.parse区分系统消息与普通消息并使用 petite-vue 的响应式列表渲染成聊天气泡对于以{开头的消息还会用 Shiki 做 JSON 语法高亮展示方便调试观察。生命周期钩子全集defineWebSocketHandler()暴露了多个钩子用于对接 WebSocket 生命周期的不同阶段详见官方文档 docs/1.docs/50.websocket.md钩子触发时机upgrade(request)连接建立之前可用于鉴权、设置命名空间、向 peer 挂载上下文数据open(peer)连接建立、peer 可收发消息时message(peer, message)收到来自 peer 的消息时close(peer, details)连接关闭时details携带可选的code与reasonerror(peer, error)连接发生错误时以upgrade为例它可以返回一个对象来精细控制升级过程属性类型说明headersHeadersInit追加到升级响应中的响应头protocolstring选择 WebSocket 子协议必须是客户端在Sec-WebSocket-Protocol中提供的值之一namespacestring覆盖本次连接的 pub/sub 命名空间contextobject附加到peer.context的数据handledboolean为true时表示钩子已自行完成升级适配层跳过默认升级逻辑upgrade最常见的用法是鉴权与身份注入校验失败时直接throw new Response(Unauthorized, { status: 401 })拒绝升级成功时返回{ context: { userId } }后续在open等钩子中即可通过peer.context.userId读取身份信息。此外依据部署适配器不同还可能有drain、ping、pong等额外钩子可用。Peer连接对象peer对象代表一个已连接的 WebSocket 客户端除upgrade外的所有钩子都能拿到它。它的属性与能力如下peer的序列化示例代码中直接用${peer}得到发送者标识如peer.toString()属性类型说明idstring该 peer 的唯一标识namespacestring该 peer 所属的 pub/sub 命名空间contextobjectupgrade阶段设置的任意上下文数据requestRequest原始的升级请求peersSetPeer同一命名空间内的所有已连接 peertopicsSetstring该 peer 已订阅的主题集合remoteAddressstring?客户端 IP依赖适配器websocketWebSocket底层 WebSocket 实例常用方法// 向单个 peer 发送消息支持字符串、对象自动 JSON 序列化与二进制数据 peer.send(Hello!); peer.send({ type: greeting, text: Hello! }); // 订阅 / 退订主题 peer.subscribe(notifications); peer.unsubscribe(notifications); // 向同一命名空间内订阅了某主题的所有 peer 广播发布者自身收不到 peer.publish(chat, { user: Alice, text: Hello everyone! }); // 优雅关闭连接发送 close 帧 peer.close(1000, Normal closure); // 立即终止连接不发送 close 帧 peer.terminate();Message消息对象message钩子收到的message对象提供了多种读取格式的方法方法返回类型说明text()string以 UTF-8 字符串读取json()T解析为 JSON支持泛型如message.json{ type: string; payload: unknown }()uint8Array()Uint8Array以字节数组读取arrayBuffer()ArrayBuffer以 ArrayBuffer 读取blob()Blob以 Blob 读取示例代码正是用message.text()判断是否包含ping再用message.toString()取出消息原文广播给其他用户。Pub/Sub主题广播与命名空间隔离Pub/sub发布/订阅是聊天室实时同步的核心机制peer 通过peer.subscribe(topic)订阅主题其他 peer 通过peer.publish(topic, data)向该主题广播所有订阅者都会收到。一个重要的语义peer.publish()会把消息发送给该主题的所有订阅者但不包括发布者自己如果需要同时让发布者看到自己的消息需要额外调用peer.send()。这正是示例代码中“先peer.send(msg)回显再peer.publish(chat, msg)广播”的原因。命名空间房间隔离命名空间为 WebSocket 连接提供隔离的 pub/sub 分组每个 peer 属于且仅属于一个命名空间peer.publish()只会在同一命名空间内广播。默认情况下命名空间由请求 URL 的路径名推导而来。这与动态路由天然配合——每个路径自动成为独立的命名空间。例如routes/rooms/[room].tsimport { defineWebSocketHandler } from nitro; export default defineWebSocketHandler({ open(peer) { peer.subscribe(messages); peer.publish(messages, ${peer} joined ${peer.namespace}); }, message(peer, message) { // 只到达同一房间内的 peer peer.publish(messages, ${peer}: ${message.text()}); }, close(peer) { peer.publish(messages, ${peer} left); }, });这样连接/rooms/game与/rooms/lobby的客户端互相隔离各自形成独立的聊天频道。若要覆盖默认命名空间可以在upgrade钩子中返回自定义namespace例如按查询参数分组import { defineWebSocketHandler } from nitro; export default defineWebSocketHandler({ upgrade(request) { const url new URL(request.url); const channel url.searchParams.get(channel) || general; return { namespace: chat:${channel}, }; }, open(peer) { peer.subscribe(messages); peer.publish(messages, ${peer} joined); }, message(peer, message) { peer.publish(messages, ${peer}: ${message.text()}); }, close(peer) { peer.publish(messages, ${peer} left); }, });客户端连接服务端就绪后浏览器端使用原生 WebSocket API 即可接入const ws new WebSocket(ws://localhost:3000/_ws); ws.addEventListener(open, () { console.log(Connected!); ws.send(Hello from client!); }); ws.addEventListener(message, (event) { console.log(Received:, event.data); });示例的 index.html 在真实项目基础上做了两处增强根据页面协议自动选择wss://或ws://前缀对消息事件中非字符串类型的数据调用event.data.text()读取兼容文本与二进制两种帧。何时改用 SSE如果只需要单向的服务器到客户端推送如通知、实时行情可以考虑用 Server-Sent EventsSSE替代 WebSocketSSE 走普通 HTTP、支持自动重连实现更简单。Nitro 通过 H3 的createEventStream提供支持import { defineHandler } from nitro; import { createEventStream } from nitro/h3; export default defineHandler((event) { const stream createEventStream(event); const interval setInterval(async () { await stream.push(Message ${new Date().toLocaleTimeString()}); }, 1000); stream.onClosed(() { clearInterval(interval); }); return stream.send(); });SSE 消息还支持结构化字段id、event、retry客户端用EventSourceAPI 即可接收。完整对比与示例可参考 docs/1.docs/50.websocket.md。小结本示例用不到 30 行代码展示了 Nitro WebSocket 的完整工作流开启features.websocket→ 用defineWebSocketHandler定义open/message/close钩子 → 通过peer.send/peer.publish/peer.subscribe组合出广播与回声语义 → 浏览器端用原生 WebSocket API 接入。再配合upgrade钩子的鉴权与命名空间隔离可以轻松扩展出带用户体系、多房间的完整实时应用。这套 API 在所有主流部署目标Node.js、Bun、Deno、Vercel、Cloudflare Workers上保持一致是构建实时应用的统一入口。【免费下载链接】nitroNext Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.项目地址: https://gitcode.com/GitHub_Trending/ni/nitro创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考