288 lines
6.6 KiB
TypeScript
288 lines
6.6 KiB
TypeScript
import type { MessageRequest, MessageResponse } from '@/types/im'
|
||
import { readonly, ref } from 'vue'
|
||
import { MessageType } from '@/types/im'
|
||
import { useUserStore } from './user'
|
||
|
||
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 userStore = useUserStore()
|
||
/* ================== 配置 ================== */
|
||
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 = userStore.token
|
||
|
||
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)
|
||
|
||
console.log('===收到消息===')
|
||
console.log(msg)
|
||
|
||
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: userStore.user.id,
|
||
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 login(token: string) {
|
||
console.log('触发login:', authenticated.value)
|
||
|
||
if (!connected.value || authenticated.value)
|
||
return
|
||
cachedToken = token
|
||
sendRaw(MessageType.LOGIN, { token })
|
||
}
|
||
|
||
function logout() {
|
||
cachedToken = null
|
||
authenticated.value = false
|
||
}
|
||
|
||
/* ================== 消息处理(核心修改点) ================== */
|
||
|
||
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,
|
||
|
||
login,
|
||
logout,
|
||
|
||
sendPrivate,
|
||
sendGroup,
|
||
|
||
onMessage,
|
||
|
||
connected: readonly(connected),
|
||
authenticated: readonly(authenticated),
|
||
}
|
||
}
|
||
|
||
/* ================== 对外 ================== */
|
||
|
||
export function useWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||
if (!singleton) {
|
||
singleton = createWebSocket(options)
|
||
}
|
||
return singleton
|
||
}
|