• 如何通过共享 WebSocket 实现多Tab的高效通信?
  • 发布于 2个月前
  • 336 热度
    0 评论
在现代 Web 应用中,WebSocket 是实现实时通信的关键技术。然而,当用户在多个标签页(Tab)中打开同一个应用时,每个 Tab 都会独立创建一个 WebSocket 连接,这会导致以下问题:
• 资源浪费:每个 WebSocket 连接都占用服务器和客户端的资源,增加了性能开销。
• 连接限制:浏览器对同一域名的 WebSocket 连接数有限制,多个连接可能导致服务不可用。
为了解决这些问题,我们可以通过共享 WebSocket 实现多 Tab 的高效通信。本文将介绍如何通过 localStorage 和 BroadcastChannel 等技术实现这一目标,并提供完整的代码示例和图表说明。

同一个浏览器多个Tab
在多 Tab 场景下,每个 Tab 都需要接收 WebSocket 消息,但独立创建 WebSocket 连接会导致资源浪费和重复消息接收的问题。根据我的web开发经验我们可以使用以下方式进行实现。

1. 主从模型:通过 localStorage 或其他机制,选定一个 Tab 作为“主标签页”(Master Tab),由它负责创建 WebSocket 连接并处理消息。
2. 消息广播:主标签页通过 BroadcastChannel 或 localStorage 将接收到的消息广播给其他 Tab。
3. 动态切换主标签页:当主标签页关闭时,其他 Tab 自动接管 WebSocket 连接。

竞选 主标签页
在竞选标签中。梦兽这里使用localStorage实现这个功能。这个是核心代码。
'use client';
import { useEffect, useRef } from'react';
import { LocalStorage } from'@/hooks';
functionuseWebSocket() {
constTAB_ID = useRef(Date.now()).current;
console.log('Client ID', TAB_ID);

const wsRef = useRef<WebSocket | null>(null);
// 主标签页的标识键
constMASTER_KEY = 'websocket_master';

// 尝试成为主标签页
functiontryBecomeMaster() {
    // 先获取当前的主标签页值
    const currentMaster = LocalStorage.getInstance().get(MASTER_KEY);

    if (!currentMaster) {
      LocalStorage.getInstance().set(MASTER_KEY, TAB_ID);
      // 双重检查,确保真的成为了主标签页
      if (LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID.toString()) {
        LocalStorage.getInstance().set(MASTER_KEY, TAB_ID);
        startWebSocket();
      }
    }
  }
// 启动 WebSocket 连接
functionstartWebSocket() {
    console.log('xxxxx');
    // 创建 WebSocket 实例
    wsRef.current = newWebSocket('wss://your-websocket-url');

    wsRef.current.onopen = function () {
      console.log('WebSocket 已连接');
    };

    wsRef.current.onmessage = function (event) {
      console.log('收到消息:', event.data);
    };
    wsRef.current.onclose = function () {
      console.log('WebSocket 已关闭');
      // 如果主标签页关闭,尝试重新成为主标签页
      // localStorage.removeItem(MASTER_KEY);
    };
  }
useEffect(() => {
    // 监听 storage 事件,检测主标签页的变化
    consthandleStorageChange = (event: StorageEvent) => {
      if (event.key === MASTER_KEY) {
        if (!event.newValue) {
          tryBecomeMaster();
        }
      }
    };

    // 在页面卸载时,释放主标签页的标识
    consthandleBeforeUnload = () => {
      console.log(LocalStorage.instance.get(MASTER_KEY), TAB_ID);
      if (LocalStorage.instance.get(MASTER_KEY) === TAB_ID) {
        LocalStorage.instance.remove(MASTER_KEY);
      }
    };
    // 堆代码 duidaima.com
    // 监听 storage 事件,检测主标签页的变化
    window.addEventListener('storage', handleStorageChange);
    // 在页面卸载时,释放主标签页的标识
    window.addEventListener('beforeunload', handleBeforeUnload);
    tryBecomeMaster();
    return() => {
      window.removeEventListener('storage', handleStorageChange);
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, []);
}
exportdefault useWebSocket;
WebSocket消息转发
const broadcastChannel = newBroadcastChannel('MASTER_MESSAGE');
const followChannel = newBroadcastChannel('FOLLOW_MESSAGE');
// ...  省略亿点代码
  wsRef.current.onmessage = function (event) {
      console.log('收到消息:', event.data);
      if(LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID) {
       broadcastChannel.postMessage(event.data);
      }
    };
     // ...  省略亿点代码

  wsRef.current.onopen = function () {
        followChannel.onMessage((event)=>{
        
         if(LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID) {
           const data = event.data;
     wsRef.send(data);
         }
    });
    };
   // ...  省略亿点代码
Change Hooks
如果不想使用使用上下文,或者状态库进行通信的话。可以封装一个hooks。再对应的组件引入。虽然会比上下文或者状态库这种方式多一点内存,但也是有优点,就是你不用关系渲染的细腻度可以自己控制到对应的组件进行监听。按需渲染
import { useEffect, useState } from'react';
import { BroadcastChannel } from'@/packages';

functionuseBroadcastChannel(channelName: string) {
const [message, setMessage] = useState();
useEffect(() => {
    const broadcastChannel = newBroadcastChannel(channelName);
    broadcastChannel.onMessage(event => {
      setMessage(event.data);
    });
  }, []);

return {
    message,
  };
}
exportdefault useBroadcastChannel;
WebSocket 消息转发的完整实现
在多 Tab 复用 WebSocket 的场景下,当主标签页接收到服务器的 WebSocket 消息时,需要将消息转发给其他标签页。通过 BroadcastChannel,我们可以高效地实现这一功能。
'use client';
import { useEffect, useRef } from'react';
import { LocalStorage } from'@/hooks';
functionuseWebSocket() {
constTAB_ID = useRef(Date.now()).current;
const wsRef = useRef<WebSocket | null>(null);
constMASTER_KEY = 'websocket_master';
const broadcastChannel = newBroadcastChannel('MASTER_MESSAGE');
// 尝试成为主标签页
functiontryBecomeMaster() {
    const currentMaster = LocalStorage.getInstance().get(MASTER_KEY);

    if (!currentMaster) {
      LocalStorage.getInstance().set(MASTER_KEY, TAB_ID);
      if (LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID.toString()) {
        startWebSocket();
      }
    }
  }
// 启动 WebSocket 连接
functionstartWebSocket() {
    wsRef.current = newWebSocket('wss://your-websocket-url');
    wsRef.current.onopen = () => {
      console.log('WebSocket 已连接');
    };
    wsRef.current.onmessage = (event) => {
      console.log('收到消息:', event.data);

      // 主标签页通过 BroadcastChannel 广播消息
      if (LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID.toString()) {
        broadcastChannel.postMessage(event.data);
      }
    };
    wsRef.current.onclose = () => {
      console.log('WebSocket 已关闭');
    };
  }
useEffect(() => {
    consthandleStorageChange = (event: StorageEvent) => {
      if (event.key === MASTER_KEY && !event.newValue) {
        tryBecomeMaster();
      }
    };
    consthandleBeforeUnload = () => {
      if (LocalStorage.getInstance().get(MASTER_KEY) === TAB_ID.toString()) {
        LocalStorage.getInstance().remove(MASTER_KEY);
      }
    };
    // 监听主标签页消息的变化
    broadcastChannel.onmessage = (event) => {
      console.log('从主标签页接收到消息:', event.data);
      // 在从标签页中处理收到的消息
    };
    window.addEventListener('storage', handleStorageChange);
    window.addEventListener('beforeunload', handleBeforeUnload);
    tryBecomeMaster();
    return() => {
      window.removeEventListener('storage', handleStorageChange);
      window.removeEventListener('beforeunload', handleBeforeUnload);
      broadcastChannel.close();
    };
  }, []);
}
exportdefault useWebSocket;
通过以上代码和思路,你可以轻松实现多 Tab 复用 WebSocket 的功能,从而提升 Web 应用的性能和用户体验。如果你在实现过程中遇到问题,欢迎随时留言讨论!
用户评论