You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

262 lines
7.9 KiB
TypeScript

1 month ago
import { ref, onMounted, onUnmounted } from 'vue';
1 month ago
import { LRUCache } from 'lru-cache';
import dayjs from 'dayjs'
import { COMMAND } from '~/utils/commandTypes'
1 month ago
let heartbeatTimer: NodeJS.Timeout;
1 month ago
// 定义 WebSocket Composable 的返回类型
interface WebSocketInstance {
ws: Ref<WebSocket | null>
status: Ref<'connecting' | 'open' | 'error' | 'closed'>
messages: Ref<{ data: string; timestamp: number }[]>
send: (name: string, json: object, token: string, cmdid: string) => void
onMessage: (callback: (data: string) => void) => () => void
connect: () => void
close: () => void
}
1 month ago
1 month ago
let instance: WebSocketInstance | null = null;
export function useWebSocket() {
if (instance) {
return instance;
}
const ws = ref<WebSocket | null>(null);
const messages = ref<{ data: string; timestamp: number }[]>([]);
const eventCallbacks: Array<(data: string) => void> = [] // 存储onMessage 回调
1 month ago
const status = ref<'connecting' | 'open' | 'error' | 'closed'>('closed');
1 month ago
// const pendingMessages = ref([]);
// 存储 { msgId: timestamp }
// const receivedMessages = new Map(); // 使用 Map 更方便记录时间
// 清理超过 5 分钟的消息 ID
// const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 分钟
const EXPIRE_TIME = 5 * 60 * 1000;// 5 分钟
1 month ago
const reconnectAttempts = ref(0);
1 month ago
// const maxReconnectAttempts = 5;
1 month ago
const isConnected = computed(() => { return status.value === 'open' });
const isConnecting = computed(() => { return status.value === 'connecting' });
const config = useRuntimeConfig();
const connectionTimeout = config.public.apiTimeout || 5000; // 5秒连接超时
1 month ago
const cache = new LRUCache({
max: 1000, // 最多 1000 个条目
ttl: EXPIRE_TIME // 5 分钟过期
});
1 month ago
// 初始化连接
const connect = () => {
if (isConnected.value || isConnecting.value) return;
status.value = 'connecting';
1 month ago
ws.value = new WebSocket(config.public.wsUrl, "json");
1 month ago
1 month ago
ws.value.onopen = () => {
1 month ago
status.value = 'open';
reconnectAttempts.value = 0;
1 month ago
// retryPendingMessages();
1 month ago
};
1 month ago
1 month ago
ws.value.onmessage = (event) => {
if (!event.data) {
return;
}
1 month ago
1 month ago
const json = JSON.parse(event.data);
const evtType = json.type;
// 连接响应不做处理
if (evtType === COMMAND.PONG || evtType === COMMAND.CONNECTED) {
if (evtType === COMMAND.CONNECTED) {
}
return;
}
const msgId = `${evtType}-${json.dateTime}`;
if (evtType !== COMMAND.SELECT_HANDLE) {
// console.log("Event Data", event.data);
}
// const now = Date.now();
// 重复ID不做处理排除选择事件
if (cache.has(msgId) && (evtType !== COMMAND.SELECT_HANDLE && evtType !== COMMAND.DRAG_ELEMENT && evtType !== COMMAND.MAP_RANGE_REAL)) {
console.warn('Duplicate message ignored:', msgId, json);
return;
}
cache.set(msgId, true);
eventCallbacks.forEach(cb => cb(event.data));
if (messages.value.length > 100) messages.value.shift();
messages.value.push({ data: event.data, timestamp: Date.now() });
1 month ago
};
1 month ago
ws.value.onerror = (error) => {
1 month ago
status.value = 'error';
1 month ago
// console.error('WebSocket发生错误:', error);
1 month ago
1 month ago
// // 重试逻辑
// if (reconnectAttempts.value < maxReconnectAttempts) {
// setTimeout(connect, 1000 * reconnectAttempts.value);
// reconnectAttempts.value++;
// }
console.error('🚨 WebSocket 错误:', error)
ws.value?.close()
1 month ago
};
1 month ago
ws.value.onclose = (event) => {
1 month ago
status.value = 'closed';
if (!event.wasClean) {
// 非正常关闭时尝试重连
1 month ago
setTimeout(connect, 3000);
1 month ago
}
1 month ago
console.log("连接关闭:", event)
1 month ago
};
}
// 等待连接就绪
const waitForConnection = () => {
return new Promise((resolve, reject) => {
if (isConnected.value) {
resolve(true);
return;
}
1 month ago
console.log("Timeout:", connectionTimeout);
1 month ago
// 设置连接超时
const timeout = setTimeout(() => {
reject(new Error('连接超时'));
}, connectionTimeout);
// 监听连接状态变化
const watchHandle = watch(isConnected, (newVal) => {
if (newVal) {
clearTimeout(timeout);
watchHandle(); // 取消监听
resolve(true);
}
}, { immediate: true });
});
};
// 发送消息
1 month ago
const send = async (name: string, json: object, token: string = '', cmdid: string = '') => {
1 month ago
const msg = {
type: name,
1 month ago
drawerToken: token || localStorage.getItem(cmdid) || '',
data: json,
cmdid: cmdid
1 month ago
}
try {
// 等待连接就绪
await waitForConnection();
1 month ago
// const ts = dayjs().unix();
// console.log("发送命令1", msg);
// const msgId = `${name}-${msg.drawerToken}-${ts}`;
// if (cache.has(msgId)) {
// return;
// }
// console.log("发送命令2", msg);
if (name !== COMMAND.PING && name !== COMMAND.MOUSE_MOVE) {
// console.log("发送消息:", msg);
}
1 month ago
1 month ago
// cache.set(msgId, ts);
ws.value.send(JSON.stringify(msg));
1 month ago
} catch (error) {
console.error('消息发送失败:', error);
// 添加到待发队列
1 month ago
// pendingMessages.value.push(JSON.stringify({ type: name, data: json }));
// throw error;
1 month ago
}
}
// 重发待处理消息
const retryPendingMessages = () => {
1 month ago
// while (pendingMessages.value.length > 0) {
// const message = pendingMessages.value.shift();
// let jsonData = JSON.parse(message);
// send(jsonData.type, jsonData.data);
// }
1 month ago
};
const startHeartbeat = () => {
heartbeatTimer = setInterval(() => {
1 month ago
if (ws.value?.readyState === WebSocket.OPEN) {
send(COMMAND.PING, {});
}
}, 10000)
}
const onMessage = (callback: any) => {
eventCallbacks.push(callback);
return () => {
const index = eventCallbacks.indexOf(callback);
if (index > -1) {
eventCallbacks.splice(index, 1);
1 month ago
}
1 month ago
}
1 month ago
}
1 month ago
// setInterval(() => {
// const now = Date.now();
// for (const [msgId, timestamp] of receivedMessages) {
// if (now - timestamp > EXPIRE_TIME) {
// receivedMessages.delete(msgId);
// }
// }
// }, CLEANUP_INTERVAL);
1 month ago
// const reconnect = ()=>{
// setTimeout(()=>{
// connect();
// },50000)
// }
1 month ago
// 手动关闭
const close = () => {
if (ws.value) {
ws.value.close()
}
}
1 month ago
onMounted(() => {
1 month ago
if (status.value === 'closed') {
connect();
}
1 month ago
startHeartbeat();
});
onUnmounted(() => {
1 month ago
// if (heartbeatTimer) {
// clearInterval(heartbeatTimer);
// };
// socket.value?.close();
1 month ago
});
1 month ago
// return { message, status, send, connect, waitForConnection }
instance = {
ws,
status,
messages,
send,
onMessage,
connect,
close
}
// 初始连接
if (typeof window !== 'undefined') {
window.addEventListener('load', connect)
}
return instance;
1 month ago
}