RN websocket

40 阅读3分钟
import { WsBaseUrl } from '@/config/env'
import { StorageType, WebsocketEventType } from '@/types'
import { DeviceEventEmitter } from 'react-native'
import { getItem } from './storage'
import { generateUUID } from '.'
import { autoResendFailRequest } from './failRequest'
import { useUserStore } from '@/store'
interface WebSocketManagerOptions {
	url: string
	protocols?: Array<string>
	reconnectTimeout?: number // 重连间隔(毫秒)
	heartbeatTimeout?: number // 心跳间隔(毫秒)
	pongTimeout?: number // 等待 pong 响应的超时时间(毫秒)
}
class WebSocketManager {
	private static instance: WebSocketManager | null = null
	private ws: WebSocket | null = null
	private heartbeatTimer: NodeJS.Timeout | null = null
	private reconnectTimer: NodeJS.Timeout | null = null

	private isReconnecting: boolean = false
	public message: string = 'ping'
	public url: string
	public protocols: Array<string> = []
	public reconnectTimeout: number
	public heartbeatTimeout: number
	public pongTimeout: number

	private constructor(options: WebSocketManagerOptions) {
		this.url = options.url
		this.protocols = options.protocols ?? []
		this.reconnectTimeout = options.reconnectTimeout ?? 15e3 // 默认 15 秒重连
		this.heartbeatTimeout = options.heartbeatTimeout ?? 15e3 // 默认 15 秒发送心跳
		this.pongTimeout = options.pongTimeout ?? 1e4 // 默认 10 秒等待 pong 响应
	}
	// 单例模式
	public static getInstance(options: WebSocketManagerOptions): WebSocketManager {
		if (!WebSocketManager.instance) {
			WebSocketManager.instance = new WebSocketManager({ ...options })
			console.log('新增的websocket实例')
			return WebSocketManager.instance
		}
		console.log('共用同一个websocket实例')
		return WebSocketManager.instance
	}

	/**
	 * 连接到 WebSocket 服务器
	 */
	connect() {
		// 如果已经存在 WebSocket 实例,则先关闭它
		if (this.ws) {
			this.ws.close(1000)
		}

		console.log('建立了连接', this.url, this.protocols)
		// 创建一个新的 WebSocket 实例,连接到指定的 URL
		this.ws = new WebSocket(this.url, this.protocols)

		// 监听 WebSocket 的 open 事件,当连接建立时触发
		this.ws.onopen = () => {
			console.log('WebSocket连接已建立')
			// 触发连接建立事件,通过 DeviceEventEmitter 发送 'websocketOpen' 事件
			DeviceEventEmitter.emit(WebsocketEventType.OPEN)
			// 连接建立后,启动心跳机制
			this.startHeartbeat()
		}

		// 监听 WebSocket 的 message 事件,当接收到消息时触发
		this.ws.onmessage = (message) => {
			if (message.data === 'pong') {
				this.resetHeartbeat()
				// 没有停服时上报
				if (!global.isErrorRepeat) {
					global.betchUpdateEventTracking()
				}

				/*** 自动发送弱网表单 */
				autoResendFailRequest()
			} else if (message.data === 'close') {
				console.log(message.data, 'message.data')
				// 后端通知关闭,前端主动关闭
				// 兼容异地登录问题
				this.close(1000)
				return false
			} else {
				console.log('发送来的消息', typeof message.data, message.data)
				const data = message?.data ? JSON.parse(message.data) : {}
				// 触发消息接收事件,通过 DeviceEventEmitter 发送 'websocketMessage' 事件,携带接收到的消息数据
				DeviceEventEmitter.emit(WebsocketEventType.MESSAGE, data)
			}
		}

		// 监听 WebSocket 的 error 事件,当发生错误时触发
		this.ws.onerror = (error) => {
			console.error('WebSocket错误:', error)
			// 触发错误事件,通过 DeviceEventEmitter 发送 'websocketError' 事件,携带错误信息
			DeviceEventEmitter.emit(WebsocketEventType.ERROR, error)
		}

		// 监听 WebSocket 的 close 事件,当连接关闭时触发
		this.ws.onclose = (event) => {
			console.log('WebSocket已关闭', event)
			// 触发连接关闭事件,通过 DeviceEventEmitter 发送 'websocketClose' 事件,携带关闭事件的详细信息
			DeviceEventEmitter.emit(WebsocketEventType.CLOSE, event)
			// 主动关闭,不重连
			if (event.code !== 1000) {
				// 连接关闭后,处理重连逻辑
				this.handleReconnect()
				global.betchUpdateEventTracking({ isForce: true })
			}
		}
	}

	// 发送消息
	send(data) {
		if (this.ws && this.ws.readyState === WebSocket.OPEN) {
			this.ws.send(data)
		} else {
			console.error('WebSocket连接未打开')
		}
	}

	// 开启心跳机制
	startHeartbeat() {
		if (this.ws && this.ws.readyState === WebSocket.OPEN) {
			// console.log('发送 ping', this.pongTimeout)
			this.ws.send(this.message)
			// 设置等待 pong 响应的超时
			this.heartbeatTimer = setTimeout(() => {
				console.error('没有收到 pong 响应,关闭连接')
				this.ws?.close(1000) // 超时未收到 pong,关闭连接
			}, this.pongTimeout)
		}
	}
	// 重置心跳(收到 pong 响应时调用)
	private resetHeartbeat(): void {
		if (this.heartbeatTimer) {
			clearTimeout(this.heartbeatTimer) // 清除之前的超时计时器
		}

		// 等待下一次发送 ping
		setTimeout(() => {
			this.startHeartbeat() // 发送下一个 ping
		}, this.heartbeatTimeout)
	}

	// 处理 WebSocket 断开后的重连
	handleReconnect() {
		if (this.isReconnecting) return // 防止重复重连
		this.isReconnecting = true

		this.reconnectTimer = setTimeout(() => {
			console.log('尝试重连 WebSocket')
			this.connect()
			this.isReconnecting = false
		}, this.reconnectTimeout)
	}

	// 关闭 WebSocket 连接
	close(code?: number) {
		if (this.ws) {
			this.ws.close(code)
			WebSocketManager.instance = null
		}
		//webSocketManager在执行方法时,多次调用导致重新生成实例
		if (!this.ws && code === 1000) {
			WebSocketManager.instance = null
		}
		if (this.heartbeatTimer) {
			clearTimeout(this.heartbeatTimer)
		}
		if (this.reconnectTimer) {
			clearTimeout(this.reconnectTimer)
		}
	}
}

// 创建并导出 WebSocket 管理器实例
const webSocketManager = () => {
	const { token, userInfo, allAppMenu, appMenu } = useUserStore.getState()
	const ID = (userInfo.ID || userInfo.id) ?? ''

	const uuid = generateUUID()
	// 如果没有用户信息或token,则不创建 WebSocket 管理器实例
	if (!ID || !token) return
	/**
	 * @description websocket 原生不支持header添加数据 故使用子协议传递参数
	 * header=[x-token,x-request-type,x-user-id,x-trace-id]
	 * x-token
	 * x-request-type  APP的时候需要传1  web端默认不填或者为0
	 * x-user-id
	 * x-trace-id
	 * app需要强制string类型
	 */
	const protocols = [token, '1', String(ID), uuid, 'json']
	console.log('protocols', protocols)
	return WebSocketManager.getInstance({
		url: `${WsBaseUrl}/sysSettingMessages/getWebSocket`,
		protocols,
	})
}

export default webSocketManager