实战指南:用 Socket.IO 组织实时通信频道)
Wasp WebSocket 命名空间Namespaces实战指南用 Socket.IO 组织实时通信频道【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp导读本指南基于 web/docs/guides/integrations/websocket-namespaces.md 展开讲解如何在 Wasp 应用中利用 Socket.IO 的命名空间Namespaces机制把实时通信逻辑拆分到独立频道如/chat、/notifications、/presence实现关注点分离。读完本文你将掌握在main.wasp.ts中配置webSocket并关闭自动连接、在服务端webSocketFn中创建命名空间、在客户端用socket.io-client直接管理连接、在 React 组件中收发事件以及在命名空间内使用 Room 做细粒度消息路由的完整实战方案同时会结合 Wasp 仓库中真实的模板源码与示例应用理解其底层的连接建立、CORS 与认证注入机制。前置知识Wasp 内建 WebSocket 支持回顾Wasp 使用 Socket.IO 在客户端与服务端之间提供完整的实时通信能力。默认情况下你只需在 Wasp 配置main.wasp.ts中声明webSocket字段并指定服务端处理函数就可以在 React 组件里通过两个内建 Hook 使用 WebSocketuseSocket()返回{ socket, isConnected }其中socket用于发送/接收事件isConnected表示 Socket.IO 连接状态。所有使用useSocket的组件共享同一个底层 socket 实例useSocketListener(event, handler)注册事件处理器并在组件卸载时自动注销避免内存泄漏。这两个 Hook 的具体实现在仓库的 SDK 模板 中可以看到useSocket本质上是useContext(WebSocketContext)而useSocketListener内部用socket.on(event, handler)注册、在 cleanup 中调用socket.off(event, handler)注销依赖数组为[event, handler]。若你尚未接触过 Wasp 的 WebSocket 基础用法建议先阅读 Web Sockets 指南再回到本指南学习命名空间。命名空间是什么为什么要用它命名空间是 Socket.IO 提供的一项功能在同一台服务器、同一个端口上把实时逻辑拆分为多个独立的通信频道。Wasp 内建支持默认只会建立一个连接默认命名空间/。当你希望分离关注点时——例如/chat只承载聊天事件、/notifications只承载通知事件——就可以使用命名空间让不同业务互不干扰、各有一套事件集合。需要特别注意的是一旦使用命名空间你就会绕过 Wasp 内建的客户端 HookuseSocket、useSocketListener改用socket.io-client直接管理连接。而服务端仍然由 Wasp 负责搭建并在你的处理函数中提供 Socket.IO 的io服务器实例。第一步在 main.wasp.ts 中启用 WebSocket 并关闭自动连接由于你将手动管理连接需要在 Wasp 配置中把autoConnect设为false。完整的配置如下源码取自 web/docs/guides/integrations/websocket-namespaces.mdimport { app, page, route } from wasp.sh/spec import Main from ./src/MainPage with { type: ref } import { webSocketFn } from ./src/websocketSetup with { type: ref } export default app({ name: WebsocketTest, wasp: { version: ^0.24.0 }, title: websocket-test, head: [link relicon href/favicon.ico /], // highlight-start webSocket: { fn: webSocketFn, autoConnect: false, }, // highlight-end spec: [ route(RootRoute, /, page(Main)), ], })参数说明fn必填指向服务端 WebSocket 处理函数的引用通过with { type: ref }导入该函数接收io和context两个参数autoConnect可选默认为true。置为false后客户端不再自动建立 WebSocket 连接连接时机完全由你掌控——这正是使用命名空间时的标准做法。这一点与 Wasp 生成的客户端模板逻辑一致在 WebSocketProvider.tsx 中默认 socket 的创建参数为io(config.apiUrl, { transports: [websocket], autoConnect: { autoConnect } !import.meta.env.SSR })即autoConnect的值直接由你在 Wasp 配置中声明的选项注入且在服务端渲染SSR场景下不会尝试建立连接。第二步创建服务端 WebSocket 处理函数在服务端webSocketFn会收到两个参数ioSocket.IO 的Server实例你可以在其上注册任意 Socket.IO 事件回调context包含 Wasp 应用中所有实体entity的 Prisma 客户端供你在实时逻辑中读写数据库。命名空间就是在这个函数里创建的通过io.of(/path)拿到一个独立的命名空间实例再对其监听connection事件import { type WebSocketDefinition } from wasp/server/webSocket; export const webSocketFn: WebSocketDefinition (io, _context) { // Create a namespace for messages const messagesNamespace io.of(/messages); messagesNamespace.on(connection, (socket) { console.log(Client connected to messages namespace); socket.on(chatMessage, (msg) { console.log(message: , msg); // Broadcast to all clients in the namespace messagesNamespace.emit(chatMessage, { id: crypto.randomUUID(), username: User, text: msg, }); }); socket.on(disconnect, () { console.log(Client disconnected from messages namespace); }); }); };这里的关键区别在于广播目标默认命名空间下你通常写io.emit(...)广播给所有客户端而在命名空间场景中必须使用messagesNamespace.emit(...)这样消息只会送达连接了/messages命名空间的客户端。socket.on(chatMessage, ...)中的msg就是客户端emit时携带的负载。仓库中的 kitchen-sink 示例 展示了同样的处理函数结构在默认命名空间下并通过WebSocketDefinitionClientToServerEvents, ServerToClientEvents, InterServerEvents泛型显式声明事件类型实现全栈类型安全export const chatWebSocket: WebSocketDefinition ClientToServerEvents, ServerToClientEvents, InterServerEvents (io, context) { io.on(connection, (socket) { const username socket.data.user?.getFirstProviderUserId() ?? Unknown; console.log(a user connected: , username); socket.on(chatMessage, async (msg) { io.emit(chatMessage, { id: uuidv4(), username, text: msg }); }); }); };注意socket.data.user如果应用启用了认证且用户已登录Wasp 会在服务端把当前用户注入到socket.data.user上详见下文底层原理一节。第三步创建客户端 WebSocket 工具模块客户端这边因为要连到自定义命名空间你不再使用 Wasp 内建的useSocket而是直接使用socket.io-client创建连接。连接地址是${config.apiUrl}/messages即 Wasp 的 API 基础地址加上命名空间路径import { useEffect, useState } from react; import { io, type Socket } from socket.io-client; import { config } from wasp/client; const messagesSocket: Socket io(${config.apiUrl}/messages, { transports: [websocket], // Vite pre-bundles socket.io-client which breaks autoConnect: https://github.com/vitejs/vite/issues/4798 autoConnect: false, }); messagesSocket.connect(); export function useMessagesSocket(): { socket: Socket; isConnected: boolean } { const [isConnected, setIsConnected] useState(messagesSocket.connected); useEffect(() { function onConnect() { setIsConnected(true); } function onDisconnect() { setIsConnected(false); } messagesSocket.on(connect, onConnect); messagesSocket.on(disconnect, onDisconnect); return () { messagesSocket.off(connect, onConnect); messagesSocket.off(disconnect, onDisconnect); }; }, []); return { socket: messagesSocket, isConnected, }; } export function useSocketListener( socket: Socket, event: string, handler: (...args: any[]) void, ) { useEffect(() { socket.on(event, handler); return () { socket.off(event, handler); }; }, [event, handler, socket]); }代码要点config.apiUrlWasp 自动生成的客户端配置指向服务端 API 地址。命名空间路径/messages需要拼接在其后transports: [websocket]与 Wasp 默认 socket 保持一致的传输方式Wasp 模板中同样强制使用 websocket 传输autoConnect: false 手动connect()注释中的链接指向 Vite 的一个已知问题——Vite 预打包socket.io-client会破坏autoConnect行为因此这里显式关闭自动连接再手动调用connect()这是规避该问题的稳妥写法useMessagesSocket封装了连接状态isConnected的订阅逻辑在组件挂载时监听connect/disconnect卸载时注销监听useSocketListener自制的监听 Hook接收任意Socket实例行为与 Wasp 内建版本一致挂载时注册、卸载时注销。第四步在 React 组件中使用命名空间连接现在把这些工具接入组件。连接状态用isConnected展示发送消息用socket.emit接收消息用自定义的useSocketListenerimport { useMessagesSocket, useSocketListener } from ./websocketHooks; const MainPage () { const { socket, isConnected } useMessagesSocket(); useSocketListener(socket, chatMessage, (message) { console.log(message received: , message); }); return ( main pStatus: {isConnected ? Connected : Disconnected}/p button onClick{() socket.emit(chatMessage, hello)} Send message /button /main ); }; export default MainPage;整个流程形成闭环点击按钮 → 客户端向/messages命名空间emit(chatMessage, hello)→ 服务端messagesNamespace.on(chatMessage, ...)收到并广播 →messagesNamespace.emit(chatMessage, { id, username, text })→ 所有已连接该命名空间的客户端包括发送者自己通过useSocketListener收到消息。这是典型的回声echo式聊天最小实现也是 Wasp 示例应用 websockets-realtime-voting 实时投票、kitchen-sink 聊天功能 所演示的基础模型。多个命名空间按业务域拆分频道命名空间的价值在多个频道并存时体现得最充分。你可以在同一个webSocketFn中创建任意多个命名空间每个命名空间拥有独立的事件集合与客户端群体export const webSocketFn: WebSocketDefinition (io, _context) { // Messages namespace const messagesNamespace io.of(/messages); messagesNamespace.on(connection, (socket) { // Handle messages events }); // Notifications namespace const notificationsNamespace io.of(/notifications); notificationsNamespace.on(connection, (socket) { // Handle notification events }); // Presence namespace const presenceNamespace io.of(/presence); presenceNamespace.on(connection, (socket) { // Handle presence events }); };客户端侧需要为每个命名空间各建立一个 socket 连接如io(config.apiUrl /notifications, ...)因为 Socket.IO 中每个命名空间是一条独立的连接通道。这样聊天消息的洪峰不会干扰通知的送达不同团队的代码也可以各自演进、互不耦合。命名空间内的 Room进一步细化消息路由命名空间负责分频道而 Socket.IO 的Room房间负责在频道内做更细粒度的消息路由。同一个命名空间下客户端可以加入特定房间消息只发给房间内的成员。这在群聊、私聊、协作编辑等场景中非常常用messagesNamespace.on(connection, (socket) { // Join a specific room socket.on(joinRoom, (roomId) { socket.join(roomId); }); // Send message to a specific room socket.on(roomMessage, ({ roomId, message }) { messagesNamespace.to(roomId).emit(chatMessage, message); }); });要点说明socket.join(roomId)把当前 socket 加入指定房间房间隶属于当前命名空间messagesNamespace.to(roomId).emit(...)只向该房间内的 socket 广播而不是整个命名空间命名空间与 Room 是正交的两个维度命名空间 顶层频道隔离Room 频道内成员分组两者叠加可以构建出非常灵活的实时架构。底层原理Wasp 是如何搭建 WebSocket 服务器的理解了用法之后再看仓库中 Wasp 生成器实际产出的模板代码可以帮你建立更扎实的心智模型。服务端初始化与 CORS在 服务端模板 initialization.ts 中Wasp 用 Node.js 的http.Server创建 Socket.IO 服务器const io /* : ServerType */ new Server(server, { cors: { origin: config.frontendUrl, } })这意味着CORS 是由 Wasp 自动配置的允许的来源直接取自主应用的config.frontendUrl你在自己代码中无需也不应该重复处理跨域问题。随后 Wasp 构造context对象把 schema 中声明的所有实体entities映射为 Prisma 客户端const context { entities: { // 每个实体: prisma.prismaIdentifier } }最后调用用户提供的处理函数await (userWebSocketFn)(io, context)webSocketFn之所以能拿到io和带实体的context正是这段生成代码注入的。认证集成sessionId 如何变成 socket.data.user在启用了认证的应用中模板会额外注入一个中间件addUserToSocketDataIfAuthenticated见 initialization.tsasync function addUserToSocketDataIfAuthenticated(socket: Socket, next: (err?: Error) void) { const sessionId socket.handshake.auth.sessionId if (sessionId) { try { const sessionAndUser await getSessionAndUserFromSessionId(sessionId) const user sessionAndUser ? makeAuthUserIfPossible(sessionAndUser.user) : null socket.data { ...socket.data, user } } catch (err) { } } next() }也就是说客户端建立连接时会把sessionId放在socket.handshake.auth.sessionId中服务端据此查询会话并得到用户对象写入socket.data.user。因此你在connection回调里可以通过socket.data.user判断当前用户身份如 kitchen-sink 的 webSocket.ts 中那样并且当你使用命名空间时这套认证注入依然生效——因为中间件注册在根命名空间的io.use(...)上Socket.IO 会让其作用于所有命名空间。客户端默认 socket 与鉴权刷新在 WebSocketProvider.tsx 中可以看到客户端默认连接的完整面貌export const socket: SocketServerToClientEvents, ClientToServerEvents io( config.apiUrl, { transports: [websocket], autoConnect: { autoConnect } !import.meta.env.SSR, } )同时它把当前sessionId注入socket.auth并监听sessionId.set/sessionId.clear事件当用户登录或登出导致会话变化时自动更新socket.auth并重连socket.auth { sessionId: getSessionId() } if (socket.connected) { socket.disconnect() socket.connect() } apiEventsEmitter.on(sessionId.set, refreshAuthToken) apiEventsEmitter.on(sessionId.clear, refreshAuthToken)这套机制同样服务于所有命名空间连接——只要你在客户端建立连接时仿照这个模式设置socket.auth { sessionId }Wasp 的getSessionId从 wasp/server 侧获取就能让自定义命名空间连接也享受登录态注入。验证与测试仓库中的真实示例仓库中已有多处可直接对照的落地实现websockets-realtime-voting 示例一个完整的实时投票应用在main.wasp.ts中声明webSocket: { fn: votingWebSocket }默认autoConnect: true其服务端逻辑位于 src/ws-server.tskitchen-sink 的聊天功能展示了带全栈类型安全的事件定义ServerToClientEvents/ClientToServerEvents/InterServerEventswebsocket.spec.ts 端到端测试用 Playwright 验证完整链路——登录后进入/chat输入 Hello World! 并点击 Send断言消息出现在页面上且包含发送者邮箱。这个测试证明无论使用默认命名空间还是自定义命名空间只要连接与事件收发配置正确端到端消息流转即可被自动化验证。如果你想在自己的命名空间应用中加入同样的端到端测试可以仿照该 spec 的结构先performLogin完成认证再page.goto到组件页面用getByRole/getByTestId交互并断言消息回显。常见问题与注意事项不要混用两套客户端 API默认命名空间下用 Wasp 的useSocket自定义命名空间下用socket.io-client。混用会导致连接管理混乱默认 socket 与自定义 socket 是不同实例。autoConnect必须显式关闭自定义命名空间连接由你手动connect()管理若保留autoConnect: true默认 socket 仍会启动造成多余连接。Vite 预打包问题socket.io-client在 Vite 下预打包会破坏自动连接务必采用本指南中的autoConnect: false 手动connect()写法代码注释中已标明此坑。广播范围命名空间内广播务必使用namespace.emit/namespace.to(room).emit否则广播会落在错误的作用域。版本说明本指南对应的 Wasp 版本为 0.24仓库中同时保留了 version-0.24 与 version-0.25 两个版本的命名空间指南跨版本行为以对应版本文档与wasp: { version }声明为准。小结命名空间是 Socket.IO 提供的成熟机制Wasp 则把服务端的搭建、CORS 与认证注入都替你处理好了你只需在webSocketFn中用io.of()声明频道、在客户端用socket.io-client直连对应路径。按照配置main.wasp.ts→ 服务端建命名空间 → 客户端建工具模块 → 组件消费四步走即可为聊天、通知、在线状态等不同业务建立彼此隔离又共享同一服务器的实时通道再叠加 Room 机制还能在频道内实现群组级消息路由足以覆盖多数实时应用的消息组织需求。【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考