这篇是使用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 // 更新主题方法
};
}