feat: 创建 useSse SSE 连接管理 composable

This commit is contained in:
2026-06-22 13:46:36 +08:00
parent aa6f0a002e
commit dfc54edd83

122
src/common/hooks/useSse.ts Normal file
View File

@@ -0,0 +1,122 @@
import { ref } from 'vue'
import { sseBus } from '@/stores/sseBus'
import { useAccountStore } from '@/stores/modules/account'
/**
* SSE 连接管理 composable
*
* 登录后调用 connect() 建立全局唯一的 SSE 长连接。
* 收到服务端事件后解析 JSON 并通过 sseBus 分发给订阅者。
* EventSource 原生自动重连,重试 3 次失败后通知用户刷新。
*
* 使用示例:
* ```ts
* const { isConnected, connect, disconnect } = useSse()
* watch(() => userStore.accessToken, (v) => v ? connect() : disconnect())
* ```
*/
export function useSse() {
/** SSE 连接是否已建立 */
const isConnected = ref(false)
/** 重试次数EventSource 每次 onerror 自增open 后归零) */
const retryCount = ref(0)
/** EventSource 实例 */
let es: EventSource | null = null
/**
* 建立 SSE 连接
*
* 从 Pinia store 获取 accessToken 拼接到 URL 查询参数。
* EventSource 不支持自定义请求头Sa-Token 支持从 ?token= 参数读取。
*/
function connect(): void {
const accountStore = useAccountStore()
const token = accountStore.accessToken
if (!token) {
console.warn('[SSE] 无 accessToken跳过连接')
return
}
// 已有连接则先关闭
if (es) {
es.close()
}
// Vite 代理: /api/sse/subscribe → 后端 /sse/subscribe
const url = `/api/sse/subscribe?token=${encodeURIComponent(token)}`
es = new EventSource(url)
es.onopen = () => {
isConnected.value = true
retryCount.value = 0
console.log('[SSE] 连接已建立')
}
// 服务端定义的业务事件类型
const eventTypes = [
'connected',
'room_created',
'room_updated',
'room_removed',
'kicked',
'game_started',
'balance_chg'
]
for (const type of eventTypes) {
es.addEventListener(type, (e: Event) => {
const msg = e as MessageEvent
// heartbeat 和 connected 确认消息可能为空数据
if (!msg.data) return
try {
const data = JSON.parse(msg.data)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sseBus.emit(type as any, data)
} catch (err) {
console.warn('[SSE] 事件数据解析失败:', type, msg.data, err)
}
})
}
// heartbeat 事件(服务端保活,前端忽略即可)
es.addEventListener('heartbeat', () => {
// NOOP: 仅用于保活
})
// 连接错误处理EventSource 会自动重连)
es.onerror = () => {
isConnected.value = false
retryCount.value++
if (retryCount.value >= 3) {
sseBus.emit('error', {
message: '实时连接异常,请刷新页面',
retryCount: retryCount.value
})
}
}
}
/**
* 断开 SSE 连接
*
* 在用户登出时调用。
*/
function disconnect(): void {
if (es) {
es.close()
es = null
}
isConnected.value = false
retryCount.value = 0
console.log('[SSE] 连接已断开')
}
return {
isConnected,
retryCount,
connect,
disconnect
}
}