refactor: replace useBackgroundOptimization with unified useBackground composable and update Nginx SSE route configuration

This commit is contained in:
jxxghp
2026-05-17 08:32:17 +08:00
parent bbe3368c69
commit 75862c026a
20 changed files with 328 additions and 147 deletions

View File

@@ -1,21 +1,33 @@
export type SSEConnectionStatus = 'idle' | 'connecting' | 'open' | 'error' | 'closed'
export interface SSEManagerOptions {
backgroundCloseDelay: number
reconnectDelay: number
maxReconnectAttempts: number
reconnectBackoffMultiplier: number
maxReconnectDelay: number
}
type SSEMessageListener = (event: MessageEvent) => void
type SSEStatusListener = (status: SSEConnectionStatus) => void
/**
* SSE连接管理器
* 优化后台SSE连接减少iOS系统杀掉应用的概率
* 统一收口 EventSource 生命周期,避免后台常驻连接和重复重连。
*/
export class SSEManager {
private eventSource: EventSource | null = null
private url: string
private isBackground = false
private isBackground = document.hidden
private reconnectTimer: number | null = null
private backgroundCloseTimer: number | null = null
private listeners: Map<string, (event: MessageEvent) => void> = new Map()
private options: {
backgroundCloseDelay: number
reconnectDelay: number
maxReconnectAttempts: number
}
private listeners: Map<string, SSEMessageListener> = new Map()
private statusListeners: Map<string, SSEStatusListener> = new Map()
private options: SSEManagerOptions
private reconnectAttempts = 0
private isConnecting = false
private isDestroyed = false
private connectionStatus: SSEConnectionStatus = 'idle'
private readonly handleVisibilityChange = () => {
if (document.hidden) {
this.handleBackground()
@@ -27,12 +39,14 @@ export class SSEManager {
this.destroy()
}
constructor(url: string, options: Partial<typeof SSEManager.prototype.options> = {}) {
constructor(url: string, options: Partial<SSEManagerOptions> = {}) {
this.url = url
this.options = {
backgroundCloseDelay: 5000, // 5秒后关闭后台连接
reconnectDelay: 3000, // 3秒后重连
maxReconnectAttempts: 3,
reconnectBackoffMultiplier: 1.5,
maxReconnectDelay: 30_000,
...options,
}
@@ -50,38 +64,44 @@ export class SSEManager {
}
private handleBackground() {
if (this.isDestroyed) return
this.isBackground = true
this.clearReconnectTimer()
// 延迟关闭SSE连接避免频繁切换
if (this.backgroundCloseTimer) {
clearTimeout(this.backgroundCloseTimer)
}
this.clearBackgroundCloseTimer()
this.backgroundCloseTimer = window.setTimeout(() => {
if (this.isBackground && this.eventSource) {
this.eventSource.close()
this.eventSource = null
this.closeCurrentEventSource()
this.setConnectionStatus('closed')
}
}, this.options.backgroundCloseDelay)
}
private handleForeground() {
if (this.isDestroyed) return
this.isBackground = false
// 清除后台关闭定时器
if (this.backgroundCloseTimer) {
clearTimeout(this.backgroundCloseTimer)
this.backgroundCloseTimer = null
}
this.clearBackgroundCloseTimer()
// 只有在有活跃监听器时才重新建立连接
if (this.listeners.size > 0 && (!this.eventSource || this.eventSource.readyState === EventSource.CLOSED)) {
this.reconnectSSE()
this.reconnectSSE(0)
}
}
private reconnectSSE(attemptCount = 0) {
if (attemptCount >= this.options.maxReconnectAttempts) {
if (this.isDestroyed || this.isBackground || this.listeners.size === 0) {
return
}
if (attemptCount > this.options.maxReconnectAttempts) {
this.reconnectAttempts = this.options.maxReconnectAttempts
this.setConnectionStatus('closed')
return
}
@@ -89,40 +109,38 @@ export class SSEManager {
return
}
// 如果没有活跃的监听器,不进行重连
if (this.listeners.size === 0) {
return
}
this.clearReconnectTimer()
this.closeCurrentEventSource()
this.isConnecting = true
this.reconnectAttempts = attemptCount
this.setConnectionStatus('connecting')
try {
this.eventSource = new EventSource(this.url)
const source = new EventSource(this.url)
this.eventSource = source
this.eventSource.onopen = () => {
source.onopen = () => {
if (source !== this.eventSource) return
this.isConnecting = false
this.reconnectAttempts = 0
this.setConnectionStatus('open')
}
this.eventSource.onerror = error => {
source.onerror = () => {
if (source !== this.eventSource) return
this.isConnecting = false
this.setConnectionStatus('error')
if (this.eventSource?.readyState === EventSource.CLOSED) {
// 连接已关闭,尝试重连
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
}
this.reconnectTimer = window.setTimeout(() => {
if (!this.isBackground && this.listeners.size > 0) {
this.reconnectSSE(this.reconnectAttempts + 1)
}
}, this.options.reconnectDelay)
if (source.readyState === EventSource.CLOSED) {
this.closeCurrentEventSource()
this.scheduleReconnect(this.reconnectAttempts + 1)
}
}
this.eventSource.onmessage = event => {
source.onmessage = event => {
if (source !== this.eventSource || this.isDestroyed) return
// 分发消息给所有监听器
this.listeners.forEach((listener, listenerId) => {
try {
@@ -135,29 +153,95 @@ export class SSEManager {
}
} catch (error) {
this.isConnecting = false
this.setConnectionStatus('error')
// 连接创建失败,尝试重连
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
}
this.reconnectTimer = window.setTimeout(() => {
if (!this.isBackground && this.listeners.size > 0) {
this.reconnectSSE(this.reconnectAttempts + 1)
}
}, this.options.reconnectDelay)
this.scheduleReconnect(this.reconnectAttempts + 1)
console.error('SSE: 连接创建失败', error)
}
}
private scheduleReconnect(attemptCount: number) {
if (this.isDestroyed || this.isBackground || this.listeners.size === 0) {
return
}
if (attemptCount > this.options.maxReconnectAttempts) {
this.reconnectAttempts = this.options.maxReconnectAttempts
this.setConnectionStatus('closed')
return
}
this.clearReconnectTimer()
this.reconnectAttempts = attemptCount
// 失败越多等待越久,避免网络波动时短时间内打满连接。
const reconnectDelay = Math.min(
this.options.reconnectDelay * this.options.reconnectBackoffMultiplier ** Math.max(0, attemptCount - 1),
this.options.maxReconnectDelay,
)
this.reconnectTimer = window.setTimeout(() => {
this.reconnectTimer = null
this.reconnectSSE(attemptCount)
}, reconnectDelay)
}
private closeCurrentEventSource() {
if (!this.eventSource) {
return
}
this.eventSource.onopen = null
this.eventSource.onerror = null
this.eventSource.onmessage = null
this.eventSource.close()
this.eventSource = null
this.isConnecting = false
}
private clearReconnectTimer() {
if (!this.reconnectTimer) return
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
private clearBackgroundCloseTimer() {
if (!this.backgroundCloseTimer) return
clearTimeout(this.backgroundCloseTimer)
this.backgroundCloseTimer = null
}
private setConnectionStatus(status: SSEConnectionStatus) {
if (this.connectionStatus === status) return
this.connectionStatus = status
this.statusListeners.forEach((listener, listenerId) => {
try {
listener(status)
} catch (error) {
console.error(`SSE: 状态监听器错误 [${listenerId}]`, error)
}
})
}
/**
* 添加消息监听器
*/
addMessageListener(id: string, listener: (event: MessageEvent) => void) {
addMessageListener(id: string, listener: SSEMessageListener) {
if (this.isDestroyed) return
this.listeners.set(id, listener)
// 如果还没有连接且不在后台,现在建立连接
if (!this.eventSource && !this.isBackground && !this.isConnecting) {
this.reconnectSSE()
if (
!this.isBackground &&
!this.isConnecting &&
(!this.eventSource || this.eventSource.readyState === EventSource.CLOSED)
) {
this.reconnectSSE(0)
}
}
@@ -165,6 +249,8 @@ export class SSEManager {
* 移除消息监听器
*/
removeMessageListener(id: string) {
if (this.isDestroyed) return
this.listeners.delete(id)
// 如果没有监听器了,关闭连接
@@ -184,25 +270,17 @@ export class SSEManager {
* 销毁管理器并清理所有引用
*/
destroy() {
if (this.isDestroyed) return
this.isDestroyed = true
this.resetConnectionState(true)
this.removeVisibilityListener()
}
private resetConnectionState(clearListeners = false) {
if (this.eventSource) {
this.eventSource.close()
this.eventSource = null
}
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
if (this.backgroundCloseTimer) {
clearTimeout(this.backgroundCloseTimer)
this.backgroundCloseTimer = null
}
this.closeCurrentEventSource()
this.clearReconnectTimer()
this.clearBackgroundCloseTimer()
if (clearListeners) {
this.listeners.clear()
@@ -210,6 +288,31 @@ export class SSEManager {
this.isConnecting = false
this.reconnectAttempts = 0
this.setConnectionStatus(this.listeners.size > 0 ? 'closed' : 'idle')
if (clearListeners) {
this.statusListeners.clear()
}
}
/**
* 添加连接状态监听器
*/
addStatusListener(id: string, listener: SSEStatusListener, emitCurrent = true) {
if (this.isDestroyed) return
this.statusListeners.set(id, listener)
if (emitCurrent) {
listener(this.connectionStatus)
}
}
/**
* 移除连接状态监听器
*/
removeStatusListener(id: string) {
this.statusListeners.delete(id)
}
/**
@@ -219,6 +322,13 @@ export class SSEManager {
return this.eventSource?.readyState ?? EventSource.CLOSED
}
/**
* 获取内部连接状态
*/
get status(): SSEConnectionStatus {
return this.connectionStatus
}
/**
* 获取连接URL
*/
@@ -230,10 +340,12 @@ export class SSEManager {
* 强制重新连接
*/
forceReconnect() {
if (this.isDestroyed) return
const hasActiveListeners = this.listeners.size > 0
this.close()
if (!this.isBackground && hasActiveListeners) {
this.reconnectSSE()
this.reconnectSSE(0)
}
}