mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-02 06:10:33 +08:00
Add background and SSE managers for improved app lifecycle management
Co-authored-by: jxxghp <jxxghp@163.com>
This commit is contained in:
276
src/utils/backgroundManager.ts
Normal file
276
src/utils/backgroundManager.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* 后台管理器
|
||||
* 统一管理定时器和后台活动,减少iOS系统杀掉应用的概率
|
||||
*/
|
||||
export class BackgroundManager {
|
||||
private timers: Map<string, {
|
||||
callback: () => void
|
||||
interval: number
|
||||
timer: ReturnType<typeof setInterval> | null
|
||||
pausedAt?: number
|
||||
runInBackground?: boolean
|
||||
}> = new Map()
|
||||
|
||||
private isBackground = false
|
||||
private isDestroyed = false
|
||||
private lastActivityTime = Date.now()
|
||||
private activityTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
constructor() {
|
||||
this.setupVisibilityListener()
|
||||
this.setupActivityTracking()
|
||||
}
|
||||
|
||||
private setupVisibilityListener() {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
const wasBackground = this.isBackground
|
||||
this.isBackground = document.hidden
|
||||
|
||||
if (this.isBackground && !wasBackground) {
|
||||
console.log('Background: 进入后台,暂停定时器')
|
||||
this.pauseAllTimers()
|
||||
} else if (!this.isBackground && wasBackground) {
|
||||
console.log('Background: 回到前台,恢复定时器')
|
||||
this.resumeAllTimers()
|
||||
}
|
||||
})
|
||||
|
||||
// 页面卸载时清理
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
private setupActivityTracking() {
|
||||
// 跟踪用户活动
|
||||
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click']
|
||||
|
||||
const updateActivity = () => {
|
||||
this.lastActivityTime = Date.now()
|
||||
}
|
||||
|
||||
events.forEach(event => {
|
||||
document.addEventListener(event, updateActivity, { passive: true })
|
||||
})
|
||||
|
||||
// 定期更新活动状态
|
||||
this.activityTimer = setInterval(() => {
|
||||
// 如果超过5分钟没有活动,可以考虑减少后台活动
|
||||
const inactiveTime = Date.now() - this.lastActivityTime
|
||||
if (inactiveTime > 5 * 60 * 1000) {
|
||||
console.log('Background: 用户长时间不活跃')
|
||||
}
|
||||
}, 60000) // 每分钟检查一次
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加定时器
|
||||
*/
|
||||
addTimer(
|
||||
id: string,
|
||||
callback: () => void,
|
||||
interval: number,
|
||||
options: {
|
||||
runInBackground?: boolean
|
||||
skipInitialRun?: boolean
|
||||
} = {}
|
||||
) {
|
||||
const { runInBackground = false, skipInitialRun = false } = options
|
||||
|
||||
this.removeTimer(id)
|
||||
|
||||
const timerConfig = {
|
||||
callback,
|
||||
interval,
|
||||
timer: null as ReturnType<typeof setInterval> | null,
|
||||
runInBackground
|
||||
}
|
||||
|
||||
// 创建定时器
|
||||
const wrappedCallback = () => {
|
||||
if (this.isDestroyed) return
|
||||
|
||||
// 只有在前台运行,或者明确允许后台运行时才执行
|
||||
if (!this.isBackground || runInBackground) {
|
||||
try {
|
||||
callback()
|
||||
} catch (error) {
|
||||
console.error(`Background: 定时器 ${id} 执行错误:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timerConfig.timer = setInterval(wrappedCallback, interval)
|
||||
this.timers.set(id, timerConfig)
|
||||
|
||||
// 如果不跳过初始运行,立即执行一次
|
||||
if (!skipInitialRun) {
|
||||
wrappedCallback()
|
||||
}
|
||||
|
||||
console.log(`Background: 添加定时器 ${id}, 间隔 ${interval}ms`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除定时器
|
||||
*/
|
||||
removeTimer(id: string) {
|
||||
const timerConfig = this.timers.get(id)
|
||||
if (timerConfig) {
|
||||
if (timerConfig.timer) {
|
||||
clearInterval(timerConfig.timer)
|
||||
}
|
||||
this.timers.delete(id)
|
||||
console.log(`Background: 移除定时器 ${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停所有定时器
|
||||
*/
|
||||
private pauseAllTimers() {
|
||||
this.timers.forEach((timerConfig, id) => {
|
||||
if (timerConfig.timer && !timerConfig.runInBackground) {
|
||||
clearInterval(timerConfig.timer)
|
||||
timerConfig.timer = null
|
||||
timerConfig.pausedAt = Date.now()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复所有定时器
|
||||
*/
|
||||
private resumeAllTimers() {
|
||||
this.timers.forEach((timerConfig, id) => {
|
||||
if (!timerConfig.timer) {
|
||||
const wrappedCallback = () => {
|
||||
if (this.isDestroyed) return
|
||||
|
||||
if (!this.isBackground || timerConfig.runInBackground) {
|
||||
try {
|
||||
timerConfig.callback()
|
||||
} catch (error) {
|
||||
console.error(`Background: 定时器 ${id} 执行错误:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timerConfig.timer = setInterval(wrappedCallback, timerConfig.interval)
|
||||
delete timerConfig.pausedAt
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取定时器状态
|
||||
*/
|
||||
getTimerStatus(id: string): 'running' | 'paused' | 'not-found' {
|
||||
const timerConfig = this.timers.get(id)
|
||||
if (!timerConfig) return 'not-found'
|
||||
return timerConfig.timer ? 'running' : 'paused'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有定时器信息
|
||||
*/
|
||||
getTimersInfo(): Array<{
|
||||
id: string
|
||||
interval: number
|
||||
status: 'running' | 'paused'
|
||||
runInBackground: boolean
|
||||
pausedAt?: number
|
||||
}> {
|
||||
return Array.from(this.timers.entries()).map(([id, config]) => ({
|
||||
id,
|
||||
interval: config.interval,
|
||||
status: config.timer ? 'running' : 'paused',
|
||||
runInBackground: config.runInBackground || false,
|
||||
pausedAt: config.pausedAt
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否活跃
|
||||
*/
|
||||
isUserActive(maxInactiveTime = 5 * 60 * 1000): boolean {
|
||||
return Date.now() - this.lastActivityTime < maxInactiveTime
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后活动时间
|
||||
*/
|
||||
getLastActivityTime(): number {
|
||||
return this.lastActivityTime
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态
|
||||
*/
|
||||
getStatus(): {
|
||||
isBackground: boolean
|
||||
isDestroyed: boolean
|
||||
timerCount: number
|
||||
lastActivityTime: number
|
||||
isUserActive: boolean
|
||||
} {
|
||||
return {
|
||||
isBackground: this.isBackground,
|
||||
isDestroyed: this.isDestroyed,
|
||||
timerCount: this.timers.size,
|
||||
lastActivityTime: this.lastActivityTime,
|
||||
isUserActive: this.isUserActive()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁管理器
|
||||
*/
|
||||
destroy() {
|
||||
this.isDestroyed = true
|
||||
|
||||
// 清理所有定时器
|
||||
this.timers.forEach((timerConfig, id) => {
|
||||
if (timerConfig.timer) {
|
||||
clearInterval(timerConfig.timer)
|
||||
}
|
||||
})
|
||||
this.timers.clear()
|
||||
|
||||
// 清理活动跟踪定时器
|
||||
if (this.activityTimer) {
|
||||
clearInterval(this.activityTimer)
|
||||
this.activityTimer = null
|
||||
}
|
||||
|
||||
console.log('Background: 管理器已销毁')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局后台管理器实例
|
||||
*/
|
||||
export const backgroundManager = new BackgroundManager()
|
||||
|
||||
/**
|
||||
* 便捷的定时器管理函数
|
||||
*/
|
||||
export function addBackgroundTimer(
|
||||
id: string,
|
||||
callback: () => void,
|
||||
interval: number,
|
||||
options?: {
|
||||
runInBackground?: boolean
|
||||
skipInitialRun?: boolean
|
||||
}
|
||||
) {
|
||||
backgroundManager.addTimer(id, callback, interval, options)
|
||||
}
|
||||
|
||||
export function removeBackgroundTimer(id: string) {
|
||||
backgroundManager.removeTimer(id)
|
||||
}
|
||||
|
||||
export function getBackgroundTimerStatus(id: string) {
|
||||
return backgroundManager.getTimerStatus(id)
|
||||
}
|
||||
221
src/utils/sseManager.ts
Normal file
221
src/utils/sseManager.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* SSE连接管理器
|
||||
* 优化后台SSE连接,减少iOS系统杀掉应用的概率
|
||||
*/
|
||||
export class SSEManager {
|
||||
private eventSource: EventSource | null = null
|
||||
private url: string
|
||||
private isBackground = false
|
||||
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
|
||||
}
|
||||
|
||||
constructor(url: string, options: Partial<typeof SSEManager.prototype.options> = {}) {
|
||||
this.url = url
|
||||
this.options = {
|
||||
backgroundCloseDelay: 5000, // 5秒后关闭后台连接
|
||||
reconnectDelay: 3000, // 3秒后重连
|
||||
maxReconnectAttempts: 3,
|
||||
...options
|
||||
}
|
||||
|
||||
this.setupVisibilityListener()
|
||||
}
|
||||
|
||||
private setupVisibilityListener() {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
this.handleBackground()
|
||||
} else {
|
||||
this.handleForeground()
|
||||
}
|
||||
})
|
||||
|
||||
// 页面卸载时关闭连接
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
|
||||
private handleBackground() {
|
||||
this.isBackground = true
|
||||
|
||||
// 延迟关闭SSE连接,避免频繁切换
|
||||
if (this.backgroundCloseTimer) {
|
||||
clearTimeout(this.backgroundCloseTimer)
|
||||
}
|
||||
|
||||
this.backgroundCloseTimer = window.setTimeout(() => {
|
||||
if (this.isBackground && this.eventSource) {
|
||||
console.log('SSE: 后台关闭连接')
|
||||
this.eventSource.close()
|
||||
this.eventSource = null
|
||||
}
|
||||
}, this.options.backgroundCloseDelay)
|
||||
}
|
||||
|
||||
private handleForeground() {
|
||||
this.isBackground = false
|
||||
|
||||
// 清除后台关闭定时器
|
||||
if (this.backgroundCloseTimer) {
|
||||
clearTimeout(this.backgroundCloseTimer)
|
||||
this.backgroundCloseTimer = null
|
||||
}
|
||||
|
||||
// 立即重新建立连接
|
||||
if (!this.eventSource || this.eventSource.readyState === EventSource.CLOSED) {
|
||||
console.log('SSE: 前台恢复连接')
|
||||
this.reconnectSSE()
|
||||
}
|
||||
}
|
||||
|
||||
private reconnectSSE(attemptCount = 0) {
|
||||
if (attemptCount >= this.options.maxReconnectAttempts) {
|
||||
console.warn('SSE: 达到最大重连次数')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.eventSource = new EventSource(this.url)
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
console.log('SSE: 连接已建立')
|
||||
}
|
||||
|
||||
this.eventSource.onerror = (error) => {
|
||||
console.error('SSE: 连接错误', error)
|
||||
|
||||
if (this.eventSource?.readyState === EventSource.CLOSED) {
|
||||
// 连接已关闭,尝试重连
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
}
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
if (!this.isBackground) {
|
||||
this.reconnectSSE(attemptCount + 1)
|
||||
}
|
||||
}, this.options.reconnectDelay)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventSource.onmessage = (event) => {
|
||||
// 分发消息给所有监听器
|
||||
this.listeners.forEach(listener => {
|
||||
try {
|
||||
listener(event)
|
||||
} catch (error) {
|
||||
console.error('SSE: 监听器错误', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('SSE: 创建连接失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息监听器
|
||||
*/
|
||||
addMessageListener(id: string, listener: (event: MessageEvent) => void) {
|
||||
this.listeners.set(id, listener)
|
||||
|
||||
// 如果还没有连接,现在建立连接
|
||||
if (!this.eventSource && !this.isBackground) {
|
||||
this.reconnectSSE()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除消息监听器
|
||||
*/
|
||||
removeMessageListener(id: string) {
|
||||
this.listeners.delete(id)
|
||||
|
||||
// 如果没有监听器了,关闭连接
|
||||
if (this.listeners.size === 0) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
close() {
|
||||
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.listeners.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
get readyState(): number {
|
||||
return this.eventSource?.readyState ?? EventSource.CLOSED
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接URL
|
||||
*/
|
||||
get connectionUrl(): string {
|
||||
return this.url
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE管理器单例
|
||||
*/
|
||||
class SSEManagerSingleton {
|
||||
private managers: Map<string, SSEManager> = new Map()
|
||||
|
||||
/**
|
||||
* 获取或创建SSE管理器
|
||||
*/
|
||||
getManager(url: string, options?: ConstructorParameters<typeof SSEManager>[1]): SSEManager {
|
||||
if (!this.managers.has(url)) {
|
||||
this.managers.set(url, new SSEManager(url, options))
|
||||
}
|
||||
return this.managers.get(url)!
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭指定URL的管理器
|
||||
*/
|
||||
closeManager(url: string) {
|
||||
const manager = this.managers.get(url)
|
||||
if (manager) {
|
||||
manager.close()
|
||||
this.managers.delete(url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有管理器
|
||||
*/
|
||||
closeAllManagers() {
|
||||
this.managers.forEach(manager => manager.close())
|
||||
this.managers.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export const sseManagerSingleton = new SSEManagerSingleton()
|
||||
Reference in New Issue
Block a user