开发项目中,用到sse链接,当新开多个标签页(>=6)的时候会耗尽当前域名的TCP链接,导致其他请求无法发送。而且也会浪费服务器资源
解决方法:
- http2,支持多路复用,一个TCP可以发起多个http请求
- 使用子域名 example.com,example.a.com
- 从代码层面解决问题,浏览器多标签页共享SSE
今天尝试从代码层面解决,需要解决两个问题
- 只能有一个标签页A发起连接
- A需要广播消息给其他标签页
只能有一个标签页A发起连接
navigator.locks 浏览器原生支持锁,函数返回一个promise,当promise兑现后释放锁,其他排队的回调才会被调用
private releaseLock: (() => void) | null = null
private attemptToBeLeader() {
navigator.locks.request(this.url, async () => {
this.isLeader = true // 标识是否是连接sse的主页面
this.launch() // 发起请求
await new Promise<void>(resolve => {
this.releaseLock = resolve // 手动调用this.releaseLock可以释放锁
})
this.isLeader = false
})
}
释放锁后下一个回调会自动执行,重新发起请求
确保只有一个标签页连接SSE服务器,当连接sse的标签页关闭/刷新(会释放锁),下一个排队的标签自动发起新的sse
广播消息
BroadcastChannel,消息发布订阅,同名的BroadcastChannel,当一个postmessage,其他会收到onmessage
constructor(
public readonly url: string
) {
if (!navigator.locks || !window.BroadcastChannel) {
throw new Error('SharedPersistentConnection is not supported in this browser')
}
this.channel = new BroadcastChannel(this.url)
this.channel.onmessage = (ev: MessageEvent<EventSourceMessage>) => {
this.config.onmessage?.(ev.data)
}
this.state = ConnectionState.Connecting
this.attemptToBeLeader()
}
// 发起请求
private launch() {
fetchEventSource(this.url, {
onmessage: (event) => {
// 广播
this.channel.postMessage(event)
},
onopen: (response) => {
this.state = ConnectionState.Open
return Promise.resolve()
},
onclose: () => {
this.close()
},
onerror: (err) => {
// 错误处理
}
})
}
整体流程
graph TD
A[用户打开 Tab 1] --> B[尝试获取连接锁]
C[用户打开 Tab 2] --> D[等待连接锁]
E[用户打开 Tab 3] --> F[等待连接锁]
B -->|获得锁| G[Tab 1 成为领导者]
D -->|等待| H[广播通道监听]
F -->|等待| H
G --> I[建立实际 SSE 连接]
I --> J[SSE 服务器]
J -->|推送数据| K[领导者 Tab 1]
K -->|通过 BroadcastChannel 广播| H
H --> L[Tab 1 接收数据]
H --> M[Tab 2 接收数据]
H --> N[Tab 3 接收数据]
完整代码:Github