封装socket坑点

tianyi Lv2

在我的那个智能协作系统中,一开始维护了一个简单的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);

//监听websocket事件
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
})
// 在构造时连接 socket
this.connect()
}

// 获取 Socket 实例(公开 getter)
public getSocket() {
return this.socket
}

// 添加事件监听
public on(event: string, handler: (...args: any[]) => void) {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, new Set())
// 实际绑定到 socket(每个事件只绑定一次)
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)
})
}

// 连接 socket
public connect() {
if (!this.socket.connected) {
this.socket.connect()
console.log('connect')
}
}

// 断开 socket
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())
// 实际绑定到 socket(每个事件只绑定一次)
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 连接,高效又资源友好。

  • Title: 封装socket坑点
  • Author: tianyi
  • Created at : 2025-04-06 14:03:46
  • Updated at : 2025-04-08 21:48:42
  • Link: https://github.com/ztygod/2025/04/06/封装socket坑点/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments