在我的那个智能协作系统中,一开始维护了一个简单的useSocket Hook(如下),但是出现问题。
这个封装方式导致了socket对同一个事件的多次监听,以至于后端emit事件的时候,前端会对这个事件进行多次响应,无法达到预期。
上面代码问题如下:
1. 多个组件共享一个 socket 实例,但都注册 connect/disconnect 监听
- 如果你多个页面/组件都调用了 useSocket(),每次都会再次注册 socket.on(…),可能出现重复事件绑定(比如控制台打印多次)的问题。而你只在 onUnmounted 清理了事件监听,但没断开连接,也没管理订阅的数量。
2. 多页面切换可能会重复调用 socket.connect()
- 每次 useSocket() 执行都会调用 socket.connect(),但 socket 其实已经连接了,这会引起无意义的重复连接或状态冲突。
总结起来两个问题:1.多组件共享这个socket导致,每次组件渲染的时候,都会再次注册socket事件导致事件被触发多次; 2.会出现无意义的重连接,同时资源得不到释放; 3.清理不彻底,未断开连接,在组件卸载时只是取消了事件监听:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { io, Socket } from "socket.io-client"; import { onUnmounted, ref } from "vue";
const SOCKET_URL = 'http://localhost:3000';
const socket: Socket = io(SOCKET_URL, { transports: ['websocket'], autoConnect: false, })
const isConnected = ref(false);
socket.on('connect', () => { isConnected.value = false; console.log('Connected to WebSocket server'); })
socket.on('disconnect', () => { isConnected.value = false; console.log('Disconnected from WebSocket server'); })
export function useSocket() { socket.connect(); onUnmounted(() => { socket.off('connect'); socket.off('disconnect'); })
return { socket, isConnected }; }
|
单例模式写法
socket.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| import { io, Socket } from 'socket.io-client'
const SOCKET_URL = 'http://localhost:3000'
class SocketManager { private socket: Socket private eventHandlers = new Map<string, Set<(...args: any[]) => void>>()
constructor() { this.socket = io(SOCKET_URL, { transports: ['websocket'], autoConnect: false }) this.connect() }
public getSocket() { return this.socket }
public on(event: string, handler: (...args: any[]) => void) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, new Set()) this.socket.on(event, (...args: any[]) => { this.eventHandlers.get(event)?.forEach(fn => fn(...args)) }) } this.eventHandlers.get(event)?.add(handler) }
public off(event: string, handler?: (...args: any[]) => void) { if (!handler) { this.eventHandlers.delete(event) this.socket.off(event) } else { this.eventHandlers.get(event)?.delete(handler) } }
public cleanup(events: Record<string, (...args: any[]) => void>) { Object.entries(events).forEach(([event, handler]) => { this.off(event, handler) }) }
public connect() { if (!this.socket.connected) { this.socket.connect() console.log('connect') } }
public disconnect() { if (this.socket.connected) { this.socket.disconnect() console.log('disconnect') } } }
export const socketManager = new SocketManager()
|
useSocket.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { ref, onUnmounted } from 'vue' import { socketManager } from '../socket'
export function useSocket() { const eventRegistry = ref<Record<string, (...args: any[]) => void>>({})
const on = (event: string, handler: (...args: any[]) => void) => { eventRegistry.value[event] = handler socketManager.on(event, handler) }
const emit = (event: string, data: any) => { socketManager.getSocket().emit(event, data) }
onUnmounted(() => { socketManager.cleanup(eventRegistry.value) })
return { socket: socketManager.getSocket(), on, emit } }
|
优点详解
1. 单例模式,避免重复连接
1
| export const socketManager = new SocketManager()
|
封装为单一对象,统一管理socket事件,避免多个组件重复连接,导致状态冲突
2. 事件管理机制清晰
保证了对一个特定事件的唯一监听。切换页面之后,后端emit socket事件,不会出现对多个回调函数的执行。使用 Map<string, Set> 来管理多个事件监听函数,且做到事件只绑定一次,防止重复注册回调。
1 2 3 4 5 6 7 8 9 10 11
| public on(event: string, handler: (...args: any[]) => void) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, new Set()) this.socket.on(event, (...args: any[]) => { this.eventHandlers.get(event)?.forEach(fn => fn(...args)) }) } this.eventHandlers.get(event)?.add(handler) }
|
3. 所有事件监听都是集中式转发
通过 SocketManager 的封装,所有事件监听都是集中式转发,避免了原生 socket.on 导致的监听混乱或覆盖问题,这使得同一个事件可以被多个组件监听,真正实现“发布-订阅”的模式。
1 2 3
| this.socket.on(event, (...args) => { this.eventHandlers.get(event)?.forEach(fn => fn(...args)) })
|
4. 支持多组件使用而不冲突
多个组件可以独立使用 useSocket(),注册自己关心的事件,互不干扰,同时共享底层唯一的 Socket 连接,高效又资源友好。