Wasp WebSocket 全栈集成实战指南:基于 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
Wasp 将 Socket.IO 深度集成进全栈框架,开发者只需在.wasp配置中声明webSocket,在服务端写一个事件处理函数,客户端即可通过useSocket/useSocketListener两个 React Hook 直接收发事件,URL 组装、CORS、连接生命周期与身份注入全部由框架接管。读完本文,你将掌握在 Wasp 0.12 中从零搭建实时聊天/投票应用、为 WebSocket 事件声明全栈类型安全,以及对照源码理解其底层接线原理的完整能力。
一、Wasp 的 WebSocket 集成方式概述
Wasp 提供“开箱即用”(batteries-included)的 WebSocket 体验,底层协议引擎是 Socket.IO。框架在服务端与客户端两侧都内置了 Socket.IO 支持,并替你处理了三件最容易出错的事:
- URL 自动配置:客户端无需手写服务端地址,
WebSocketProvider直接复用 Wasp 生成的config.apiUrl建立连接; - CORS 自动开启:服务端
new Server(...)时自动将config.frontendUrl设为cors.origin; - React 抽象:提供
useSocket与useSocketListener两个 Hook,封装连接的建立、状态上报与事件监听/卸载。
要启用 WebSocket,只需四步:
- 在服务端定义 WebSocket 事件处理逻辑;
- 在 Wasp 配置文件的
app中声明webSocket并绑定服务端函数; - 在客户端 React 组件中通过
useSocket、useSocketListener使用; - 可选:为事件与载荷声明 TypeScript 类型,获得全栈类型安全。
下文从第 2 步(配置文件)开始逐步展开。
二、在 Wasp 配置文件中开启 WebSocket
在app声明中新增webSocket字典,并为其提供必需的fn(服务端函数引用)。autoConnect为可选项,控制客户端是否自动建立连接,默认值为true。
app todoApp { // ... webSocket: { fn: import { webSocketFn } from "@src/webSocket", autoConnect: true, // optional, default: true }, }这里的语法是经典的.waspDSL 形式:import { webSocketFn } from "@src/webSocket"指向src/webSocket.js(或.ts)中导出的函数。需要注意的是,当前仓库的示例项目(如 examples/websockets-realtime-voting/main.wasp.ts)已采用新一代 TypeScript 规范(TS Spec)写法,声明等价:
export default app({ // ... webSocket: { fn: votingWebSocket, }, // ... });从 AppSpec 的解析模型可以看出webSocket字典只有两个字段:fn(ExtImport,服务端函数的外部导入引用)与可选的autoConnect(Maybe Bool),见 waspc/src/Wasp/AppSpec/App/WebSocket.hs。生成器据此判断“是否启用了 WebSocket”:只要app.webSocket存在即视为启用,见 waspc/src/Wasp/Generator/WebSocket.hs。
webSocket配置字段速查
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
fn | WebSocketFn | 是 | 定义 WebSocket 事件与处理函数的服务端函数 |
autoConnect | bool | 否 | 客户端是否自动连接 WebSocket 服务端,默认true |
依赖自动注入
启用 WebSocket 后,生成器会为服务端与 SDK 自动声明所需的 npm 依赖(版本范围为^4.6.1),无需手动安装:
- 服务端:
socket.io、@socket.io/component-emitter; - SDK 侧:额外增加
socket.io-client。
这一逻辑定义在 waspc/src/Wasp/Generator/WebSocket.hs,并由 waspc/src/Wasp/Generator/ServerGenerator/WebSocketG.hs 与 waspc/src/Wasp/Generator/SdkGenerator/WebSocketGenerator.hs 分别装配进服务端与 SDK 的 package.json。
三、定义服务端事件处理函数webSocketFn
在src/webSocket.js(或.ts)中定义服务端 WebSocket 逻辑。函数签名为(io, context) => void:
io:Socket.IO 的Server实例,可用它注册connection回调及所有标准 Socket.IO 事件;context:提供 Wasp 应用的全部实体(entities),可直接在事件回调中使用,例如context.entities.SomeEntity.create(...)。
若用户已登录,服务端 socket 上还会附带socket.data.user。
JavaScript 版本
import { v4 as uuidv4 } from 'uuid' import { getFirstProviderUserId } from 'wasp/auth' export const webSocketFn = (io, context) => { io.on('connection', (socket) => { const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown' console.log('a user connected: ', username) socket.on('chatMessage', async (msg) => { console.log('message: ', msg) io.emit('chatMessage', { id: uuidv4(), username, text: msg }) // You can also use your entities here: // await context.entities.SomeEntity.create({ someField: msg }) }) }) }要点解析:
getFirstProviderUserId(socket.data.user)从当前登录用户的 Auth 实体中取出用户名(?? 'Unknown'处理匿名兜底);socket.on('chatMessage', ...)监听单个客户端事件;io.emit('chatMessage', ...)广播给所有连接;- 事件回调是
async的,因此在其中可以安全地await context.entities.*做数据库操作。
TypeScript 版本与全栈类型安全
TS 版本的关键在于用WebSocketDefinition泛型显式声明四组类型参数,从而在服务端定义事件契约,客户端自动继承:
import { v4 as uuidv4 } from 'uuid' import { getFirstProviderUserId } from 'wasp/auth' import { type WebSocketDefinition, type WaspSocketData } from 'wasp/server/webSocket' export const webSocketFn: WebSocketFn = (io, context) => { io.on('connection', (socket) => { const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown' console.log('a user connected: ', username) socket.on('chatMessage', async (msg) => { console.log('message: ', msg) io.emit('chatMessage', { id: uuidv4(), username, text: msg }) // You can also use your entities here: // await context.entities.SomeEntity.create({ someField: msg }) }) }) } // Typing our WebSocket function with the events and payloads // allows us to get type safety on the client as well type WebSocketFn = WebSocketDefinition< ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData > interface ServerToClientEvents { chatMessage: (msg: { id: string, username: string, text: string }) => void; } interface ClientToServerEvents { chatMessage: (msg: string) => void; } interface InterServerEvents {} // Data that is attached to the socket. // NOTE: Wasp automatically injects the JWT into the connection, // and if present/valid, the server adds a user to the socket. interface SocketData extends WaspSocketData {}WebSocketDefinition的类型定义可在 SDK 模板中看到,它约束了io的四个泛型参数,并声明context.entities由 Wasp 生成的全部实体组成:
export type WebSocketDefinition< ClientToServerEvents extends EventsMap = DefaultEventsMap, ServerToClientEvents extends EventsMap = DefaultEventsMap, InterServerEvents extends EventsMap = DefaultEventsMap, SocketData extends WaspSocketData = WaspSocketData > = ( io: Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>, context: { entities: { /* 所有实体 */ } } ) => Promise<void> | void见 waspc/data/Generator/templates/sdk/wasp/server/webSocket/index.ts。
关于SocketData还有一处值得注意的框架行为:Wasp 会自动把当前会话的 sessionId 注入握手认证(socket.auth),服务端若校验通过,会把用户对象附加到socket.data.user上。WaspSocketData接口正是为此预留的结构(启用 auth 时包含可选字段user?: AuthUser),见 waspc/data/Generator/templates/sdk/wasp/server/webSocket/index.ts。其底层实现是服务端初始化模板中的中间件addUserToSocketDataIfAuthenticated:读取socket.handshake.auth.sessionId,经会话查询得到用户后再写入socket.data,见 waspc/data/Generator/templates/server/src/webSocket/initialization.ts。
四、客户端使用:useSocket与useSocketListener
客户端 WebSocket 能力从wasp/client/webSocket导入,核心是WebSocketProvider+ 两个 Hook(SDK 源码见 waspc/data/Generator/templates/sdk/wasp/client/webSocket/index.ts 与 WebSocketProvider.tsx)。
useSocketHook
useSocket()返回一个对象:
socket: Socket:用于发送与接收事件的 Socket.IO 客户端实例;isConnected: boolean:Socket.IO 连接状态,适合在界面上展示连接指示灯。
两点重要行为:
- 默认自动连接:Wasp 默认会自动建立客户端到服务端的 WebSocket 连接,无需手动调用
socket.connect()/socket.disconnect(); autoConnect: false时:若你在 Wasp 文件中关闭了自动连接,则需按需自行调用这两个方法。
此外,所有使用useSocket的组件共享同一个底层socket单例。从模板源码可以看到,socket是在模块顶层通过io(config.apiUrl, { transports: ['websocket'], autoConnect: ... })创建的:
export const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io( config.apiUrl, { transports: ['websocket'], autoConnect: {= autoConnect =} && !import.meta.env.SSR, } )见 WebSocketProvider.tsx。其中{= autoConnect =}是模板占位符,由 SDK 生成器根据webSocket.autoConnect配置填充——只有未显式设置为false时才为true,见 waspc/src/Wasp/Generator/SdkGenerator/WebSocketGenerator.hs。同时!import.meta.env.SSR保证了 SSR 场景下不会在服务端创建 WebSocket 连接。
模板还在模块加载时调用refreshAuthToken():把当前sessionId写入socket.auth,并监听sessionId.set/sessionId.clear事件,在登录/登出后自动重连以刷新认证信息,见 WebSocketProvider.tsx。
useSocketListenerHook
useSocketListener: (event, callback) => void用于注册事件处理器,并在组件卸载时自动注销监听,无需手动清理。
useSocketListener('chatMessage', logMessage)其实现基于useEffect,在 effect 中执行socket.on(event, handler)并返回socket.off(event, handler)作为清理函数,依赖为[event, handler],见 index.ts。
完整聊天页面示例
JavaScript 版本
import React, { useState } from 'react' import { useSocket, useSocketListener, } from 'wasp/client/webSocket' export const ChatPage = () => { const [messageText, setMessageText] = useState('') const [messages, setMessages] = useState([]) const { socket, isConnected } = useSocket() useSocketListener('chatMessage', logMessage) function logMessage(msg) { setMessages((priorMessages) => [msg, ...priorMessages]) } function handleSubmit(e) { e.preventDefault() socket.emit('chatMessage', messageText) setMessageText('') } const messageList = messages.map((msg) => ( <li key={msg.id}> <em>{msg.username}</em>: {msg.text} </li> )) const connectionIcon = isConnected ? '🟢' : '🔴' return ( <> <h2>Chat {connectionIcon}</h2> <div> <form onSubmit={handleSubmit}> <div> <div> <input type="text" value={messageText} onChange={(e) => setMessageText(e.target.value)} /> </div> <div> <button type="submit">Submit</button> </div> </div> </form> <ul>{messageList}</ul> </div> </> ) }TypeScript 版本(全栈类型安全生效)
TS 场景下,所有事件与载荷类型会自动从服务端推断并下发到客户端。在 VS Code 中,事件名与载荷都会有自动补全,写错事件名或载荷类型会直接得到类型错误。你还可以使用两个辅助类型来获取指定事件的载荷类型:
ClientToServerPayload<'eventName'>:客户端 → 服务端某事件的载荷类型;ServerToClientPayload<'eventName'>:服务端 → 客户端某事件的载荷类型。
这两个辅助类型的定义为Parameters<Events[Event]>[0],见 index.ts。
import React, { useState } from 'react' import { useSocket, useSocketListener, ServerToClientPayload, } from 'wasp/client/webSocket' export const ChatPage = () => { const [messageText, setMessageText] = useState< // We are using a helper type to get the payload type for the "chatMessage" event. ClientToServerPayload<'chatMessage'> >('') const [messages, setMessages] = useState< ServerToClientPayload<'chatMessage'>[] >([]) // The "socket" instance is typed with the types you defined on the server. const { socket, isConnected } = useSocket() // This is a type-safe event handler: "chatMessage" event and its payload type // are defined on the server. useSocketListener('chatMessage', logMessage) function logMessage(msg: ServerToClientPayload<'chatMessage'>) { setMessages((priorMessages) => [msg, ...priorMessages]) } function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() // This is a type-safe event emitter: "chatMessage" event and its payload type // are defined on the server. socket.emit('chatMessage', messageText) setMessageText('') } const messageList = messages.map((msg) => ( <li key={msg.id}> <em>{msg.username}</em>: {msg.text} </li> )) const connectionIcon = isConnected ? '🟢' : '🔴' return ( <> <h2>Chat {connectionIcon}</h2> <div> <form onSubmit={handleSubmit}> <div> <div> <input type="text" value={messageText} onChange={(e) => setMessageText(e.target.value)} /> </div> <div> <button type="submit">Submit</button> </div> </div> </form> <ul>{messageList}</ul> </div> </> ) }注意:ClientToServerPayload在示例代码中已使用但未在import中显式列出,实际使用时请一并从wasp/client/webSocket导入。
五、源码视角:WebSocket 是如何被接线的
理解生成器如何把配置“翻译”成运行时代码,有助于排查问题与扩展高级用法。
服务端接线
- 生成器在检测到
app.webSocket后,基于模板 server/src/webSocket/initialization.ts 生成服务端初始化代码,见 waspc/src/Wasp/Generator/ServerGenerator/WebSocketG.hs; - 初始化逻辑:
new Server(server, { cors: { origin: config.frontendUrl } })绑定到 HTTP server,自动开启对前端地址的 CORS(无需手写跨域配置);随后(启用 auth 时)注册鉴权中间件,把用户注入socket.data;再构造context = { entities: { ...所有实体 } },最后调用你的webSocketFn(io, context),见 initialization.ts; genWebSockets仅在启用了 WebSocket 时才生成上述文件(否则为空列表),并在ServerGenerator的生成流程中被调用,见 waspc/src/Wasp/Generator/ServerGenerator.hs。
SDK 侧接线
SDK 生成器同样按需生成三类文件:服务端类型索引(server/webSocket/index.ts)、客户端 Hook(client/webSocket/index.ts)与 Provider(client/webSocket/WebSocketProvider.tsx),见 waspc/src/Wasp/Generator/SdkGenerator/WebSocketGenerator.hs。这些文件会在SdkGenerator主流程中与其余 SDK 文件一并产出,见 waspc/src/Wasp/Generator/SdkGenerator.hs。
依赖版本
框架固定 Socket.IO 相关依赖的版本范围为socket.io ^4.6.1、@socket.io/component-emitter ^4.0.0,统一在 waspc/src/Wasp/Generator/WebSocket.hs 中声明,保证服务端与客户端协议版本一致。
六、实战参考:仓库内的实时投票示例
当前仓库自带一个完整的 WebSocket 实时应用示例 examples/websockets-realtime-voting,可作为对照实现:
- 配置声明:
app中声明webSocket: { fn: votingWebSocket },并启用了usernameAndPassword认证与authRequired页面,见 examples/websockets-realtime-voting/main.wasp.ts; - 服务端:
votingWebSocket用WebSocketDefinition<ClientToServerEvents, ServerToClientEvents, InterServerEvents>声明vote、askForStateUpdate两个客户端事件与updateState服务端事件,在内存中维护轮询状态,支持“投票/改票”,见 examples/websockets-realtime-voting/src/ws-server.ts; - 客户端:
MainPage.tsx通过useSocketListener('updateState', ...)订阅状态、socket.emit('vote', optionId)投票,并利用ServerToClientPayload<'updateState'>获得状态载荷的完整类型,见 examples/websockets-realtime-voting/src/pages/MainPage.tsx; - 端到端验证:
e2e-tests/tests/simple.spec.ts用 Playwright 验证了“注册 → 登录 → 投票 → 界面出现用户名、按钮变为 Voted 且禁用、票数更新”的完整链路,见 examples/websockets-realtime-voting/e2e-tests/tests/simple.spec.ts,可作为自己应用集成测试的模板。
值得注意:示例服务端在connection回调中先检查socket.data.user,未登录连接直接返回——这是匿名连接控制的一种实用做法;服务端以io.emit广播updateState,保证所有客户端实时看到投票结果。
七、常见问题与最佳实践
- 不需要手动管理连接:默认
autoConnect: true,登录/登出后框架会依据sessionId事件自动刷新认证并重连;只有显式设置autoConnect: false时才需要手动socket.connect()/socket.disconnect()。 - 连接状态展示:用
isConnected驱动 UI 指示灯(如聊天页的 🟢/🔴),其状态由 Provider 在connect/disconnect事件中维护(初始为false,保证 SSR 安全),见 WebSocketProvider.tsx。 - 类型安全优先用 TS:把事件契约定义在服务端
WebSocketDefinition中,客户端事件名与载荷即可获得自动补全与编译期校验,避免“拼错事件名”这类运行时低级错误。 - 在事件回调中使用数据库:
context.entities可用且回调为async,可直接持久化业务数据,不必再额外发起 RPC。 - 事件监听清理:优先使用
useSocketListener,它已封装好卸载时注销;若在组件中直接socket.on,请自行在useEffect清理,避免内存泄漏与重复回调。
至此,从配置声明、服务端事件定义、客户端 Hook 使用到源码级接线原理,你已具备在 Wasp 0.12 中构建实时功能(聊天、投票、通知、协作编辑等)的完整知识。更详细的 API 字段与版本差异可继续查阅本文档原文 web/versioned_docs/version-0.12/advanced/web-sockets.md,以及仓库内更新的版本化文档 web/docs/advanced/web-sockets.md。
【免费下载链接】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),仅供参考