This commit is contained in:
oneao committed 2025-12-28 22:35:36 +08:00
1 parent e98ab295c4
commit 92fb1b74a7
22 files changed
+1204 -45

No files matched your search

@@ -0,0 +1,289 @@
import type { MessageRequest, MessageResponse } from '@/types/im'
import { readonly, ref } from 'vue'
import { MessageType } from '@/types/im'
interface UseWebSocketOptions {
url: string
heartbeatInterval?: number
pongTimeout?: number
reconnectInterval?: number
maxReconnectTimes?: number
}
/* ================== 单例 ================== */
let singleton: ReturnType<typeof createWebSocket> | null = null
function createWebSocket(options?: Partial<UseWebSocketOptions>) {
/* ================== 配置 ================== */
const config: Required<UseWebSocketOptions> = {
url:
options?.url
|| import.meta.env.VITE_WEBSOCKET_BASEURL
|| 'ws://127.0.0.1:8090/ws',
heartbeatInterval: options?.heartbeatInterval ?? 15_000,
pongTimeout: options?.pongTimeout ?? 5_000,
reconnectInterval: options?.reconnectInterval ?? 3_000,
maxReconnectTimes: options?.maxReconnectTimes ?? 5,
}
/* ================== 状态 ================== */
let ws: WebSocket | null = null
const connected = ref(false)
const authenticated = ref(false)
let cachedToken: string | null = null
let heartbeatTimer: number | null = null
let pongTimeoutTimer: number | null = null
let reconnectTimer: number | null = null
let reconnectTimes = 0
let manualClose = false
/* ================== 外部消息回调 ================== */
const messageHandlers: Array<(msg: MessageResponse) => void> = []
/* ================== 连接管理 ================== */
function connect() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
console.log('[WebSocket] 已存在连接或正在连接,跳过')
return
}
console.log('[WebSocket] 开始建立连接')
manualClose = false
ws = new WebSocket(config.url)
ws.onopen = () => {
connected.value = true
reconnectTimes = 0
console.log('[WebSocket] 连接成功')
startHeartbeat()
// 自动重新登录
if (cachedToken) {
console.log('[WebSocket] 自动重新发送 LOGIN')
login(cachedToken)
}
}
ws.onmessage = (evt) => {
const msg: MessageResponse = JSON.parse(evt.data)
handleMessage(msg)
}
ws.onclose = () => {
console.warn('[WebSocket] 连接关闭')
connected.value = false
authenticated.value = false
stopHeartbeat()
stopPongTimeout()
ws = null
if (!manualClose) {
tryReconnect()
}
}
ws.onerror = (err) => {
console.error('[WebSocket] 连接错误', err)
}
}
function close() {
console.log('[WebSocket] 主动关闭连接')
manualClose = true
stopHeartbeat()
stopPongTimeout()
clearReconnectTimer()
ws?.close()
ws = null
}
/* ================== 重连 ================== */
function tryReconnect() {
if (reconnectTimer || reconnectTimes >= config.maxReconnectTimes)
return
reconnectTimes++
console.warn(`[WebSocket] 第 ${reconnectTimes} 次重连中...`)
reconnectTimer = window.setTimeout(() => {
reconnectTimer = null
connect()
}, config.reconnectInterval)
}
function clearReconnectTimer() {
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
}
/* ================== 心跳 ================== */
function startHeartbeat() {
stopHeartbeat()
heartbeatTimer = window.setTimeout(() => {
sendRaw(MessageType.PING)
startPongTimeout()
startHeartbeat()
}, config.heartbeatInterval)
}
function stopHeartbeat() {
if (heartbeatTimer) {
clearTimeout(heartbeatTimer)
heartbeatTimer = null
}
}
function startPongTimeout() {
stopPongTimeout()
pongTimeoutTimer = window.setTimeout(() => {
console.error('[WebSocket] 心跳超时,断开连接')
ws?.close()
}, config.pongTimeout)
}
function stopPongTimeout() {
if (pongTimeoutTimer) {
clearTimeout(pongTimeoutTimer)
pongTimeoutTimer = null
}
}
/* ================== 发送 ================== */
function sendRaw(type: MessageType, data?: any, to?: any) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.warn('[WebSocket] 发送失败,未连接')
return
}
const msg: MessageRequest = {
type,
from: 0,
to: to ?? null,
timestamp: Date.now(),
data: data ?? null,
}
ws.send(JSON.stringify(msg))
}
function send(type: MessageType, data?: any, to?: any) {
if (!authenticated.value && type !== MessageType.LOGIN) {
console.warn('[WebSocket] 未 LOGIN,禁止发送业务消息')
return
}
sendRaw(type, data, to)
}
/* ================== 登录相关 ================== */
function setAuthToken(token: string | null) {
cachedToken = token
if (token && connected.value) {
login(token)
}
}
function login(token: string) {
if (!connected.value || authenticated.value)
return
cachedToken = token
sendRaw(MessageType.LOGIN, { token })
}
function logout() {
cachedToken = null
authenticated.value = false
sendRaw(MessageType.LOGOUT)
}
/* ================== 消息处理(核心修改点) ================== */
function handleMessage(msg: MessageResponse) {
switch (msg.type) {
case MessageType.PONG:
stopPongTimeout()
return
case MessageType.LOGIN:
authenticated.value = true
console.log('[WebSocket] LOGIN 成功')
return
case MessageType.LOGOUT:
authenticated.value = false
console.log('[WebSocket] 已 LOGOUT')
return
default:
// 所有业务消息交给外部
messageHandlers.forEach(cb => cb(msg))
}
}
/* ================== 对外订阅 ================== */
function onMessage(handler: (msg: MessageResponse) => void) {
messageHandlers.push(handler)
}
/* ================== 业务 API ================== */
function sendPrivate(toUserId: number, content: string) {
send(MessageType.PRIVATE, { content }, toUserId)
}
function sendGroup(groupId: number, content: string) {
send(MessageType.GROUP, { groupId, content })
}
/* ================== 初始化 ================== */
connect()
return {
connect,
close,
setAuthToken,
login,
logout,
sendPrivate,
sendGroup,
onMessage,
connected: readonly(connected),
authenticated: readonly(authenticated),
}
}
/* ================== 对外 ================== */
export function useWebSocket(options?: Partial<UseWebSocketOptions>) {
if (!singleton) {
singleton = createWebSocket(options)
}
return singleton
}