问题的根源
在 SPA(单页应用)中,页面路由切换时不会刷新整个页面,但组件会被销毁和重建。如果在多个页面组件中都 new WebSocketClient() 创建实例,即使使用相同的 URL,也会建立多条独立的 WebSocket 连接。
每条 WebSocket 连接都会占用一个 TCP 端口和服务器端的内存资源。用户频繁切换路由时,连接数会不断累积。虽然在 SPA 内可以通过代码控制,但如果用 new 的方式创建实例,很难避免重复连接。
单例模式的实现
单例模式确保同一个 URL 只创建一个 WebSocketClient 实例。实现方式是使用静态 Map 缓存已创建的实例:
export class WebSocketClient {
// 静态属性,存储所有已创建的实例,key 为 URL
private static clients: Record<string, WebSocketClient> = {}
// 静态方法,获取或创建实例
static getInstance(options: WebSocketClientOptions | string): WebSocketClient {
// 如果传入的是字符串,当作 URL 处理
if (typeof options === 'string') {
if (!WebSocketClient.clients[options]) {
const client = new WebSocketClient({ url: options })
WebSocketClient.clients[options] = client
}
return WebSocketClient.clients[options]
}
// 如果传入的是对象
if (!WebSocketClient.clients[options.url]) {
const client = new WebSocketClient(options)
WebSocketClient.clients[options.url] = client
return client
}
return WebSocketClient.clients[options.url]
}
}
typescript
使用方式:
// 方式一:传入 URL 字符串
const client1 = WebSocketClient.getInstance('ws://localhost:3031')
// 方式二:传入配置对象
const client2 = WebSocketClient.getInstance({
url: 'ws://localhost:3031',
pingInterval: 30000,
onMessage: (event, data) => console.log(data)
})
// 验证是否为同一实例
console.log(client1 === client2) // true(如果 URL 相同)
typescript
验证单例模式的效果
// 在不同的页面/组件中
const ws1 = WebSocketClient.getInstance('ws://localhost:3031')
const ws2 = WebSocketClient.getInstance('ws://localhost:3031')
const ws3 = WebSocketClient.getInstance({ url: 'ws://localhost:3031' })
console.log(ws1 === ws2) // true
console.log(ws2 === ws3) // true
typescript
在 Network 面板中只会看到一条 WebSocket 连接,而不是三条。控制台打印出两个 true,证明它们引用的是同一个对象。
JavaScript 中,两个变量引用同一个对象时,使用 ===(严格相等)比较会返回 true,因为比较的是内存地址。这就是单例模式的验证依据。
单例模式的局限
单例模式解决了 SPA 应用内部的路由切换问题,但无法解决多 Tab 的问题。当用户在浏览器中打开多个标签页时,每个标签页都是独立的 JavaScript 执行环境,静态变量无法跨标签页共享。
打开两个标签页后,即使都使用了 getInstance,服务端日志会显示两个客户端连接,clientsMap 中会有两个条目。这是下一个要解决的问题——通过 SharedWorker 实现跨 Tab 的实例共享。
↑