在现代 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 应用的性能和用户体验。如果你在实现过程中遇到问题,欢迎随时留言讨论!