直接用原生 websocket 会因状态失控导致静默失败、消息乱序、类型丢失等问题;必须封装连接生命周期、心跳、重连、类型校验与错误兜底机制。

直接用 WebSocket 原生 API 写业务,不出三天就会遇到连接中断不重连、消息乱序、类型丢失、重复订阅、onmessage 里无法访问 this 等问题。TypeScript 封装不是为了炫技,而是把这几类高频故障点收口控制。
为什么不能直接 new WebSocket() 后就发消息?
原生 WebSocket 实例在 readyState === WebSocket.CONNECTING 或 CLOSED 时调用 send() 会静默失败,甚至抛出 InvalidStateError。浏览器不会帮你等连接就绪,也不会自动恢复断线。
- 必须手动监听
onopen才能安全发送第一条消息 -
onerror不保证触发(比如网络闪断时可能只走onclose) - 多个组件同时调用
send()但共用一个 socket 实例,容易因状态不同步导致消息发不出 - 没有内置心跳,服务端超时踢人后前端毫无感知
封装时必须处理的 4 个核心状态流转
基于 WebSocketStatus 枚举和 readyState 的映射关系,封装类要主动管理连接生命周期,而不是被动响应事件。
- 从
CONNECTING到OPEN:触发onOpen回调,并清空待发队列 - 从
OPEN到CLOSING:禁止新消息入队,允许已排队消息尝试发送 - 从
OPEN到CLOSED:若配置了autoReconnect: true,延迟reconnectInterval后重试,且不超过maxReconnectAttempts - 收到
onmessage时:必须用JSON.parse(event.data)并校验结构,再按type字段分发给对应处理器,不能直接透传原始字符串
如何让 sendMessage 支持泛型 + 类型守卫?
关键不是“能发”,而是“发出去的东西,接收方一定能正确 decode”。靠文档或注释约定不如靠类型系统强制约束。
- 定义
WebSocketMessage<t></t>接口,要求所有消息带type和data: T,避免payload、body、content混用 -
sendMessage方法签名应为sendMessage<t>(message: WebSocketMessage<t>): void</t></t>,而非sendMessage(data: any) - 在发送前做一次
if (typeof message !== 'object' || !('type' in message))检查,防止传入 null 或原始类型导致JSON.stringify出错 - 服务端返回的消息也必须严格符合该接口,否则
onMessage回调里的T泛型会失去意义
Pinia store 中管理 WebSocket 实例的坑
别把 WebSocket 实例直接塞进 state。它不可序列化,会导致 SSR 失败、devtools 显示异常、store 热更新崩溃。
- WebSocket 实例应存在
store的私有属性(如private socket: WebSocket | null = null)中,不参与响应式追踪 - 用
computed暴露status(映射自socket?.readyState),而不是直接暴露socket - 在
onUnmounted或 store$dispose时显式调用socket.close(),否则标签页关闭后连接仍可能残留 - 多个页面/模块需要同一连接时,用单例模式 + 引用计数管理连接生命周期,而不是每个 store 都 new 一个
最易被忽略的是错误边界的处理:服务端返回非 JSON 格式数据、message.type 值拼写错误、重连时旧 socket 还没 close 新的已 open —— 这些都不会抛异常,但会让整个通信链路静默失效。封装的价值,正在于把这些“不报错的错”提前兜住。











