uniapp自定义WebSocket连接hook

34 阅读2分钟

 这是使用uniapp原生方法连接的WebSocket,所以需要跟后端沟通信息接收与发送的信息格式

import { ref, onMounted, onUnmounted } from 'vue';

export default function useUniWebSocket(
	url : string,
	onMessage : (res : any) => void,
	heartbeatInterval = 30000,
) {
	const socketTask = ref<UniApp.SocketTask | null>(null);
	const isDisconnect = ref(false);
	const isConnected = ref(false);
	const reconnectAttempts = ref(0);
	const maxReconnectAttempts = 5;
	const topic = ref('/topic/global'); // 默认订阅主题
	const subscriptionActive = ref(false); // 跟踪订阅状态
	const heartbeatCount = ref(0); // 添加心跳计数器
	const heartbeatStatus = ref('inactive'); // 'active' | 'inactive'

	let heartbeatTimer : number | null = null;
	let reconnectTimer : number | null = null;

	// 清理资源
	const cleanup = () => {
		if (heartbeatTimer) {
			clearInterval(heartbeatTimer);
			heartbeatTimer = null;
		}
		if (reconnectTimer) {
			clearTimeout(reconnectTimer);
			reconnectTimer = null;
		}
		reconnectAttempts.value = 0;
		subscriptionActive.value = false;
		heartbeatStatus.value = 'inactive';
	};

	// 修复后的心跳函数
	const startHeartbeat = () => {
		// 关键修复:清除现有定时器
		if (heartbeatTimer) {
			clearInterval(heartbeatTimer);
			heartbeatTimer = null;
		}

		heartbeatTimer = setInterval(() => {
			if (socketTask.value && isConnected.value) {
				socketTask.value.send({
					data: JSON.stringify({
						type: 'heartbeat',
						timestamp: Date.now(),
						count: heartbeatCount.value++ // 添加计数跟踪
					}),
					success: () => console.log('♥️ 心跳发送成功'),
					fail: (err) => console.error('心跳发送失败:', err)
				});
				sendMessage('我发送信息了')
			}
		}, heartbeatInterval) as unknown as number;
		heartbeatStatus.value = 'active';
	};

	// 初始化WebSocket连接
	const connect = () => {
		if (isConnected.value || isDisconnect.value) return;

		console.log('🔌 WebSocket连接中...', url);
		isConnected.value = false;
		subscriptionActive.value = false;

		// 关闭现有连接(如果存在)
		if (socketTask.value) {
			try {
				socketTask.value.close({});
			} catch (e) {
				console.warn('关闭旧连接时出错:', e);
			}
			socketTask.value = null;
		}

		try {
			socketTask.value = uni.connectSocket({
				url,
				success: () => console.log('🟢 WebSocket创建成功'),
				fail: (err) => {
					console.error('🔴 WebSocket创建失败:', err);
					handleReconnect();
				}
			});

			// 事件监听
			socketTask.value.onOpen((res:any) => {
				console.log('✅ WebSocket连接已打开回调',res);
				console.log('✅ WebSocket连接已打开');
				isConnected.value = true;
				reconnectAttempts.value = 0;
				startHeartbeat();
				subscribeTopic(); // 连接成功后订阅主题
			});

			socketTask.value.onClose((res) => {
				console.log('⛔ WebSocket连接关闭', res);
				isConnected.value = false;
				subscriptionActive.value = false;
				cleanup();

				if (!isDisconnect.value && reconnectAttempts.value < maxReconnectAttempts) {
					handleReconnect();
				}
			});

			socketTask.value.onError((error) => {
				console.error('❌ WebSocket错误:', error);
				isConnected.value = false;
				handleReconnect();
			});

			socketTask.value.onMessage((res : { data : string }) => {
				try {
					console.log('📥 收到原始消息:', res.data);

					// 尝试解析JSON
					let data;
					try {
						data = JSON.parse(res.data);
					} catch (e) {
						// 如果解析失败,保持原始数据
						data = res.data;
					}

					// 检查是否是订阅确认消息
					if (data && data.type === 'subscribed' && data.topic === topic.value) {
						console.log(`✅ 主题订阅确认: ${topic.value}`);
						subscriptionActive.value = true;
						return;
					}

					// 检查是否是心跳响应
					if (data && data.type === 'heartbeat_ack') {
						console.log('❤️‍🔥 收到心跳响应');
						return;
					}

					// 传递给外部处理程序
					onMessage(data);
				} catch (e) {
					console.error('❗ 消息处理失败:', e, '原始数据:', res.data);
				}
			});

		} catch (error) {
			console.error('创建WebSocket时异常:', error);
			handleReconnect();
		}
	};

	// 处理重连逻辑
	const handleReconnect = () => {
		if (isDisconnect.value || reconnectAttempts.value >= maxReconnectAttempts) {
			console.warn('停止重连,已达最大尝试次数');
			return;
		}

		cleanup();
		reconnectAttempts.value++;

		const delay = Math.min(3000 * reconnectAttempts.value, 15000);
		console.log(`⌛ ${delay}ms 后尝试重连 (#${reconnectAttempts.value})`);

		reconnectTimer = setTimeout(() => {
			connect();
		}, delay) as unknown as number;
	};

	// 订阅主题
	const subscribeTopic = () => {
		if (!socketTask.value || !isConnected.value) {
			console.warn('WebSocket 尚未连接,无法订阅主题');
			return false;
		}

		if (subscriptionActive.value) {
			console.log('主题已订阅,无需重复订阅');
			return true;
		}

		const subscriptionMessage = JSON.stringify({
			type: 'subscribe',
			topic: topic.value,
			timestamp: Date.now()
		});

		socketTask.value.send({
			data: subscriptionMessage,
			success: (res:any) => {
				console.log(`📤 订阅成功回调: ${res}`);
				console.log(`📤 订阅请求已发送: ${topic.value}`);
			},
			fail: (err) => {
				console.error(`❌ 订阅请求发送失败: ${topic.value}`, err);
			}
		});

		return true;
	};

	// 更新订阅主题
	const updateTopic = (newTopicValue : string) => {
		topic.value = newTopicValue;
		subscriptionActive.value = false; // 重置订阅状态

		if (isConnected.value) {
			subscribeTopic();
		}
	};

	// 断开连接
	const disconnect = () => {
		console.log('主动断开WebSocket连接');
		isDisconnect.value = true;
		isConnected.value = false;
		subscriptionActive.value = false;

		if (socketTask.value) {
			try {
				// 发送取消订阅消息
				if (subscriptionActive.value) {
					const unsubscribeMessage = JSON.stringify({
						type: 'unsubscribe',
						topic: topic.value
					});

					socketTask.value.send({
						data: unsubscribeMessage,
						success: () => console.log(`取消订阅请求已发送: ${topic.value}`),
						fail: (err) => console.error(`取消订阅请求发送失败: ${err}`)
					});
				}

				// 关闭连接
				socketTask.value.close({ reason: '用户主动断开' });
			} catch (e) {
				console.error('❌ 关闭连接时出错:', e);
			}
			socketTask.value = null;
		}
		cleanup();
	};

	// 发送消息
	const sendMessage = (message : any) => {
		console.log(message);
		return new Promise<void>((resolve, reject) => {
			if (!socketTask.value || !isConnected.value) {
				reject('WebSocket未连接');
				return;
			}

			// const payload = typeof message === 'string'
			// 	? message
			// 	: JSON.stringify(message);
				
			const payload = JSON.stringify({
				type: 'custom',
				topic: '/app',
				timestamp: Date.now(),
				content:message
			});

			socketTask.value.send({
				data: payload,
				success: () => resolve(),
				fail: (err) => reject(err)
			});
		});
	};

	// 生命周期钩子
	onMounted(() => {
		isDisconnect.value = false;
		connect();
	});

	onUnmounted(() => {
		disconnect();
	});

	return {
		socketTask,
		isConnected,
		subscriptionActive,
		connect,
		disconnect,
		sendMessage,
		subscribe: subscribeTopic,
		updateTopic,
		reconnectAttempts
	};
}