uniapp使用stomp.js链接WebSocket连接hook

36 阅读5分钟

这篇是使用stomp.js组件库连接WebSocket,为了兼容后端框架使用。但由于uni-app的 SocketTask 与 STOMP 客户端不兼容。所以需要写一个适配器

import { ref, onMounted, onUnmounted } from 'vue';
import { Client, type IFrame } from '@stomp/stompjs';

/**
 * uni-app WebSocket 适配器类
 * 提供与标准 WebSocket API 兼容的接口,适配 uni-app 的 SocketTask
 */
class UniWebSocket {
    private socketTask: UniApp.SocketTask | null = null; // uni-app 的 WebSocket 实例
    public onopen: (() => void) | null = null;           // 连接打开回调
    public onmessage: ((event: { data: string }) => void) | null = null; // 消息接收回调
    public onclose: ((event: { code: number; reason: string }) => void) | null = null; // 连接关闭回调
    public onerror: ((err: any) => void) | null = null;  // 错误处理回调
    public readyState = 0; // 连接状态:0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED

    constructor(url: string) {
        this.readyState = 0; // 初始状态为连接中
        
        try {
            // 创建 uni-app WebSocket 连接
            this.socketTask = uni.connectSocket({
                url,
                success: () => console.log('🟢 WebSocket 创建成功'),
                fail: (err) => {
                    console.error('🔴 WebSocket 创建失败:', err);
                    this.onerror?.(err);
                }
            });

            // 连接打开事件
            this.socketTask.onOpen(() => {
                console.log('✅ WebSocket 连接已打开');
                this.readyState = 1; // 更新状态为已打开
                this.onopen?.(); // 触发打开回调
            });

            // 消息接收事件
            this.socketTask.onMessage((res) => {
                // 过滤心跳消息
                if (res.data && res.data.includes('"type":"heartbeat"')) {
                    console.log('❤️ 收到心跳响应');
                    return;
                }
                this.onmessage?.({ data: res.data }); // 触发消息回调
            });

            // 连接关闭事件
            this.socketTask.onClose((res) => {
                console.log(`⛔ WebSocket 关闭 (code: ${res.code}, reason: ${res.reason})`);
                this.readyState = 3; // 更新状态为已关闭
                this.onclose?.({ code: res.code, reason: res.reason }); // 触发关闭回调
            });

            // 错误事件
            this.socketTask.onError((error) => {
                console.error('❌ WebSocket 错误:', error);
                this.onerror?.(error); // 触发错误回调
            });
        } catch (error) {
            console.error('创建 WebSocket 时出错:', error);
            this.onerror?.(error);
        }
    }

    /**
     * 发送消息
     * @param data 要发送的数据
     */
    send(data: string) {
        if (this.socketTask && this.readyState === 1) {
            return new Promise<void>((resolve, reject) => {
                this.socketTask!.send({
                    data,
                    success: () => resolve(),
                    fail: (err) => reject(err)
                });
            });
        } else {
            return Promise.reject('WebSocket not connected');
        }
    }

    /**
     * 关闭连接
     * @param code 关闭代码 (可选)
     * @param reason 关闭原因 (可选)
     */
    close(code?: number, reason?: string) {
        if (this.socketTask && this.readyState === 1) {
            this.readyState = 2; // 更新状态为关闭中
            this.socketTask.close({
                code: code || 1000,
                reason: reason || 'Normal closure'
            });
        }
    }
}

/**
 * 使用 STOMP over WebSocket 的自定义 Hook
 * @param url WebSocket 服务器地址
 * @param onMessage 消息接收回调函数
 * @param heartbeatInterval 心跳间隔 (毫秒),默认 10000
 * @returns 包含连接状态和操作方法的对象
 */
export default function useStompWebSocket(
    url: string,
    onMessage: (res: any) => void,
    heartbeatInterval = 10000
) {
    // 响应式状态变量
    const stompClient = ref<Client | null>(null);         // STOMP 客户端实例
    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 subscriptionId = ref<string | null>(null);      // 订阅ID
    const isConnecting = ref(false);                      // 是否正在连接中
    const lastError = ref<string | null>(null);           // 最后发生的错误信息
    const protocolVersion = ref('1.2');                   // STOMP 协议版本
    const connectionAttempt = ref(0);                     // 连接尝试次数计数器

    // 定时器引用
    let reconnectTimer: number | null = null;             // 重连定时器
    let heartbeatTimeout: number | null = null;           // 心跳超时定时器

    /**
     * 清理资源
     * 清除定时器并重置状态
     */
    const cleanup = () => {
        // 清除重连定时器
        if (reconnectTimer) {
            clearTimeout(reconnectTimer);
            reconnectTimer = null;
        }
        // 清除心跳超时定时器
        if (heartbeatTimeout) {
            clearTimeout(heartbeatTimeout);
            heartbeatTimeout = null;
        }
        // 重置状态
        subscriptionActive.value = false;
        lastError.value = null;
    };

    /**
     * 心跳监控
     * 检测心跳是否超时,如果超时则断开连接
     */
    const startHeartbeatMonitor = () => {
        // 清除现有心跳超时定时器
        if (heartbeatTimeout) {
            clearTimeout(heartbeatTimeout);
        }
        
        // 设置心跳超时时间(比发送间隔长一点)
        const timeout = heartbeatInterval + 5000;
        
        // 设置新的心跳超时定时器
        heartbeatTimeout = setTimeout(() => {
            if (isConnected.value) {
                console.error('💔 心跳超时,连接可能已断开');
                lastError.value = '心跳超时,连接已断开';
                handleDisconnection();
            }
        }, timeout) as unknown as number;
    };

    /**
     * 处理断开连接
     * 重置状态并尝试重连
     */
    const handleDisconnection = () => {
        // 更新连接状态
        isConnected.value = false;
        isConnecting.value = false;
        subscriptionActive.value = false;
        
        // 清理 STOMP 客户端
        if (stompClient.value) {
            try {
                stompClient.value.deactivate();
            } catch (e) {
                console.warn('断开连接时出错:', e);
            }
            stompClient.value = null;
        }
        
        // 如果不是手动断开且未达到最大重连次数,则尝试重连
        if (!isDisconnect.value && reconnectAttempts.value < maxReconnectAttempts) {
            handleReconnect();
        }
    };

    /**
     * 初始化 STOMP 连接
     * 创建并激活 STOMP 客户端
     */
    const connect = () => {
        // 修复:在连接前重置 isDisconnect 状态
        if (isDisconnect.value) {
            console.log('🔃 重置断开状态,允许重新连接');
            isDisconnect.value = false;
        }
        
        // 如果已连接或正在手动断开,则不再尝试连接
        if (isConnected.value || isDisconnect.value) return;
        
        console.log('🔌 STOMP连接中...', url);
        // 更新连接状态
        isConnecting.value = true;
        isConnected.value = false;
        subscriptionActive.value = false;
        connectionAttempt.value++;
        
        // 清理现有连接(如果存在)
        if (stompClient.value) {
            try {
                stompClient.value.deactivate();
            } catch (e) {
                console.warn('清理旧连接时出错:', e);
            }
            stompClient.value = null;
        }

        // 创建新的 STOMP 客户端
        stompClient.value = new Client({
            brokerURL: url,                     // WebSocket 服务器地址
            reconnectDelay: 0,                   // 禁用内置重连,使用自定义重连逻辑
            heartbeatIncoming: 0,                // 不期望服务器心跳
            heartbeatOutgoing: heartbeatInterval, // 客户端发送心跳间隔
            debug: (str) => console.log('STOMP DEBUG:', str), // 调试日志
            
            // 使用 uni-app 适配器
            webSocketFactory: () => new UniWebSocket(url) as any,
            
            // 连接头信息
            connectHeaders: {
                'accept-version': protocolVersion.value, // 支持的协议版本
                'heart-beat': `${heartbeatInterval},0`   // 心跳配置(发送,不接收)
            },
            
            // 连接成功回调
            onConnect: (frame: IFrame) => {
                console.log('✅ STOMP连接成功', frame);
                console.log('协议版本:', frame.headers.version);
                console.log('服务器心跳:', frame.headers['heart-beat']);
                
                // 更新连接状态
                isConnected.value = true;
                isConnecting.value = false;
                reconnectAttempts.value = 0;
                
                // 启动心跳监控
                startHeartbeatMonitor();
                
                // 订阅主题
                subscribeTopic();
            },
            
            // STOMP 协议错误处理
            onStompError: (frame: IFrame) => {
                const errorMsg = frame.headers.message || 'STOMP协议错误';
                console.error('❌ STOMP协议错误:', errorMsg);
                console.error('错误详情:', frame.body);
                
                // 保存错误信息
                lastError.value = `STOMP错误: ${errorMsg}`;
                
                // 协议版本降级策略
                if (errorMsg.includes('version')) {
                    if (protocolVersion.value === '1.2') {
                        console.warn('尝试降级到STOMP 1.1协议');
                        protocolVersion.value = '1.1';
                    } else if (protocolVersion.value === '1.1') {
                        console.warn('尝试降级到STOMP 1.0协议');
                        protocolVersion.value = '1.0';
                    }
                }
                
                // 处理断开连接
                handleDisconnection();
            },
            
            // WebSocket 错误处理
            onWebSocketError: (event: Event) => {
                console.error('❌ WebSocket错误:', event);
                lastError.value = `WebSocket错误: ${event.type}`;
                handleDisconnection();
            },
            
            // 连接断开回调
            onDisconnect: (frame) => {
                console.log('⛔ STOMP连接关闭', frame);
                lastError.value = frame.headers.message || '连接已关闭';
                handleDisconnection();
            },
            
            // 连接前回调
            beforeConnect: () => {
                console.log('⌛ 正在建立连接...');
            }
        });

        // 激活客户端连接
        stompClient.value.activate();
    };

    /**
     * 处理重连逻辑
     * 实现指数退避重连策略
     */
    const handleReconnect = () => {
        // 如果手动断开或达到最大重连次数,则不再重连
        if (isDisconnect.value || reconnectAttempts.value >= maxReconnectAttempts) {
            console.warn('停止重连,已达最大尝试次数');
            return;
        }

        // 清理资源
        cleanup();
        reconnectAttempts.value++;

        // 指数退避重连策略(2^attempts * 2000,最大30秒)
        const delay = Math.min(2000 * Math.pow(2, reconnectAttempts.value), 30000);
        console.log(`⌛ ${delay}ms 后尝试重连 (#${reconnectAttempts.value})`);

        // 设置重连定时器
        reconnectTimer = setTimeout(() => {
            connect();
        }, delay) as unknown as number;
    };

    /**
     * 订阅主题
     * @returns 订阅是否成功
     */
    const subscribeTopic = () => {
        // 检查连接状态
        if (!stompClient.value || !isConnected.value) {
            console.warn('STOMP 尚未连接,无法订阅主题');
            return false;
        }

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

        try {
            // 生成唯一订阅ID
            const id = `sub-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
            subscriptionId.value = id;

            // 执行订阅
            stompClient.value.subscribe(
                topic.value, // 订阅的主题
                (message) => {
                    try {
                        console.log('📥 收到STOMP消息:', message.body);
                        
                        // 重置心跳监控(每次收到消息都重置)
                        startHeartbeatMonitor();
                        
                        // 解析消息体
                        let data;
                        try {
                            data = JSON.parse(message.body);
                        } catch (e) {
                            // 如果解析失败,保持原始数据
                            data = message.body;
                        }
                        
                        // 传递给外部处理程序
                        onMessage(data);
                    } catch (e) {
                        console.error('❗ 消息处理失败:', e, '原始数据:', message.body);
                    }
                },
                { id, ack: 'auto' } // 订阅选项(ID和自动确认模式)
            );

            // 更新订阅状态
            subscriptionActive.value = true;
            console.log(`✅ 已订阅主题: ${topic.value}`);
            return true;
        } catch (error) {
            console.error(`❌ 订阅失败: ${topic.value}`, error);
            return false;
        }
    };

    /**
     * 更新订阅主题
     * @param newTopicValue 新的主题值
     */
    const updateTopic = (newTopicValue: string) => {
        // 取消旧订阅(如果存在)
        if (subscriptionId.value && stompClient.value) {
            stompClient.value.unsubscribe(subscriptionId.value);
            subscriptionActive.value = false;
            console.log(`已取消订阅: ${topic.value}`);
        }

        // 更新主题
        topic.value = newTopicValue;

        // 如果已连接,则订阅新主题
        if (isConnected.value) {
            subscribeTopic();
        }
    };

    /**
     * 断开连接
     * 手动关闭 WebSocket 连接
     */
    const disconnect = () => {
        console.log('主动断开STOMP连接');
        // 更新状态
        isDisconnect.value = true;
        isConnected.value = false;
        isConnecting.value = false;
        subscriptionActive.value = false;

        // 清理 STOMP 客户端
        if (stompClient.value) {
            try {
                // 取消订阅
                if (subscriptionId.value) {
                    stompClient.value.unsubscribe(subscriptionId.value);
                    console.log(`已取消订阅: ${topic.value}`);
                }

                // 断开连接
                stompClient.value.deactivate();
            } catch (e) {
                console.error('❌ 断开连接时出错:', e);
            }
            stompClient.value = null;
        }
        // 清理资源
        cleanup();
    };

    /**
     * 发送消息
     * @param message 要发送的消息内容
     * @returns Promise 表示发送操作的结果
     */
    const sendMessage = (message: any) => {
        return new Promise<void>((resolve, reject) => {
            // 检查连接状态
            if (!stompClient.value || !isConnected.value) {
                reject('STOMP未连接');
                return;
            }

            try {
                // 确保目标地址格式正确
                const destination = topic.value.startsWith('/app') 
                    ? topic.value 
                    : `/app${topic.value}`;

                // 发布消息
                stompClient.value.publish({
                    destination, // 目标地址
                    body: JSON.stringify({ // 消息体
                        type: 'custom',
                        timestamp: Date.now(),
                        content: message
                    }),
                    headers: { 
                        'content-type': 'application/json',
                        'persistent': 'true' // 持久化消息
                    }
                });
                console.log('📤 消息发送成功:', message);
                resolve();
            } catch (err) {
                console.error('❌ 消息发送失败:', err);
                reject(err);
            }
        });
    };

    // 生命周期钩子:组件挂载时连接
    onMounted(() => {
        isDisconnect.value = false;
        connect();
    });

    // 生命周期钩子:组件卸载时断开连接
    onUnmounted(() => {
        disconnect();
    });

    // 返回公开的API
    return {
        isConnected,          // 是否已连接
        isConnecting,         // 是否正在连接中
        subscriptionActive,   // 订阅是否激活
        lastError,            // 最后发生的错误
        reconnectAttempts,    // 重连尝试次数
        protocolVersion,      // 当前协议版本
        connect,              // 连接方法
        disconnect,           // 断开连接方法
        sendMessage,          // 发送消息方法
        subscribe: subscribeTopic, // 订阅主题方法
        updateTopic           // 更新主题方法
    };
}