React 里 WebSocket 的正确管理:多标签页、断线重连与状态同步

test12026-09-110 次阅读

为什么不能直接写在组件里

很多人第一次在 React 里接 WebSocket,会本能地写在某个组件的 useEffect 里:挂载时 new 一个连接,卸载时 close 掉。功能短时间能跑,但页面一多、用户习惯开三四个标签页,问题就来了:连接数爆掉、消息重复接收、刷新后短暂掉线、多个组件各连各的。

最常见的写法有三个问题。第一,每个用到这个 Hook 的组件都会建一条独立连接,一个页面就三四条,浏览器对单域并发连接是有上限的。第二,组件卸载就 close,切个路由连接就断,切回来又重连,中间消息会丢。第三,用户开多个标签页,每个标签页是独立 JS 上下文,各自建连接、各自重连,后端一抖,所有标签页同时重连,瞬间形成重连风暴,反而把后端打挂。

核心结论:WebSocket 连接应该是"应用级单例",而不是"组件级局部变量"。

第一步:连接管理器做成单例

把连接从组件里抽出来,放到一个模块级的单例对象里。组件不关心连接怎么建、建几次,只关心"订阅消息"和"发送消息"。

class ConnectionManager {
  private ws: WebSocket | null = null;
  private listeners = new Set<(data: any) => void>();
  private url: string;
  private closedByUser = false;

  constructor(url: string) { this.url = url; }

  connect() {
    if (this.ws && this.ws.readyState <= WebSocket.OPEN) return;
    this.closedByUser = false;
    this.ws = new WebSocket(this.url);
    this.ws.onmessage = (e) => {
      const data = JSON.parse(e.data);
      this.listeners.forEach((fn) => fn(data));
    };
    this.ws.onclose = () => {
      if (!this.closedByUser) this.scheduleReconnect();
    };
  }

  subscribe(fn: (data: any) => void) {
    this.listeners.add(fn);
    this.connect();
    return () => this.listeners.delete(fn);
  }

  send(data: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  }
}

这样无论多少组件、多少处订阅,底层始终只有一条连接。

第二步:指数退避重连

onclose 里不能直接无脑重连,否则后端一抖,前端高频重试会把情况搞得更糟。用指数退避,每次重连间隔翻倍,并设上限。重连成功后重置间隔,避免"抖一次之后 forever 慢重连"。

第三步:多标签页状态同步

单例解决了单标签页内共用一条连接,但每个标签页仍是独立连接。要做到多设备、多标签互不干扰,需要在协议层带上 device 字段,让后端按连接聚合,而不是按用户粗暴地设一个在线标志。前端这边,跨标签页状态同步可以用 BroadcastChannel:一个标签页收到的消息广播给同源其他标签页,避免重复触发侧边栏红点动画,再配合 visibilitychange 在页面回到前台时主动检查连接。

第四步:包成 React Hook

前面都是框架无关的纯 TS,最后给组件一个干净接口:组件里完全看不到 WebSocket 怎么建、怎么重连,只管收发。新页面接入实时能力,就是调一个 Hook 的事。

小结

连接单例化,别写在组件里各自建连;重连用指数退避,别无脑重试;多标签页用单连接加 BroadcastChannel 同步;设备级状态交给协议层的 device 字段。把连接当成基础设施而不是组件状态,实时功能的稳定性会好很多。

T

test1

文章作者

为什么不能直接写在组件里 很多人第一次在 React 里接 WebSocket,会本能地写在某个组件的 useEffe...

分类
技术
发布时间
2026-09-11
字数
约 1891 字
阅读
0 次