Files
JoyD/Web/Vue/WebSocketClient/WebSocketClient.vue
2025-10-22 22:09:49 +08:00

252 lines
6.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>import { ref, onUnmounted, watch } from 'vue';
// 定义组件的props
const props = defineProps({
url: {
type: String,
default: 'ws://localhost:8805'
},
protocols: {
type: Array,
default: () => ['device-web']
},
autoConnect: {
type: Boolean,
default: true
},
maxReconnectAttempts: {
type: Number,
default: 0
},
initialReconnectDelay: {
type: Number,
default: 1000
},
maxReconnectDelay: {
type: Number,
default: 30000
}
});
// 定义组件的emits
const emit = defineEmits([
'connect', // 连接成功时触发
'disconnect', // 断开连接时触发
'message', // 收到消息时触发
'error', // 发生错误时触发
'status-change' // 连接状态改变时触发
]);
// 内部状态管理
const ws = ref(null);
const isConnected = ref(false);
const reconnectAttempts = ref(0);
const reconnectDelay = ref(props.initialReconnectDelay);
const reconnectTimer = ref(null);
const manualDisconnect = ref(false);
const currentUrl = ref(props.url);
// 调试日志函数
const debugLog = (...args) => {
if (window.DEBUG) {
console.log('[WebSocketClient]', ...args);
}
};
// 更新连接状态并发出状态改变事件
const updateConnectionStatus = (connected) => {
if (isConnected.value !== connected) {
isConnected.value = connected;
emit('status-change', connected);
if (connected) {
// 连接成功时发出连接事件
emit('connect', currentUrl.value);
// 重置重连状态
reconnectAttempts.value = 0;
reconnectDelay.value = props.initialReconnectDelay;
} else {
ws.value = null;
}
}
};
// 取消当前的重连尝试
const cancelReconnect = () => {
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value);
reconnectTimer.value = null;
}
};
// 智能重连策略
const reconnect = () => {
// 如果是手动断开,不进行重连
if (manualDisconnect.value) {
return;
}
// 达到最大重连次数且最大重连次数不为0时停止重连
// 当maxReconnectAttempts为0时表示无限重试
if (props.maxReconnectAttempts > 0 && reconnectAttempts.value >= props.maxReconnectAttempts) {
debugLog('已达到最大重连次数');
emit('error', '已达到最大重连次数,请手动尝试重新连接');
return;
}
// 指数退避重连延迟
const delay = Math.min(reconnectDelay.value * Math.pow(2, reconnectAttempts.value), props.maxReconnectDelay);
// 调整日志显示当maxReconnectAttempts为0时显示"无限重试"模式
if (props.maxReconnectAttempts === 0) {
debugLog(`无限重试模式:将在${Math.round(delay/1000)}秒后尝试第${reconnectAttempts.value + 1}次重连`);
} else {
debugLog(`将在${Math.round(delay/1000)}秒后尝试第${reconnectAttempts.value + 1}/${props.maxReconnectAttempts}次重连`);
}
reconnectTimer.value = setTimeout(() => {
reconnectAttempts.value++;
connect();
}, delay);
};
// 连接WebSocket服务器
const connect = (url = null) => {
// 如果提供了新的URL则更新当前URL
if (url) {
currentUrl.value = url;
}
manualDisconnect.value = false;
cancelReconnect();
try {
debugLog(`尝试连接到: ${currentUrl.value}`);
ws.value = new WebSocket(currentUrl.value, props.protocols);
// 连接成功
ws.value.onopen = () => {
debugLog('连接成功');
updateConnectionStatus(true);
};
// 接收消息
ws.value.onmessage = (event) => {
debugLog('收到消息:', event.data);
emit('message', event.data);
};
// 连接关闭
ws.value.onclose = (event) => {
debugLog(`连接关闭: 代码=${event.code}, 原因=${event.reason}`);
updateConnectionStatus(false);
emit('disconnect', event.code, event.reason);
// 异常断开时自动重连
if (event.code === 1006) {
reconnect();
} else if (!manualDisconnect.value) {
// 非手动断开且非1006错误时也尝试重连
reconnect();
}
};
// 连接错误
ws.value.onerror = (error) => {
debugLog('WebSocket错误:', error);
emit('error', '连接错误,请检查服务器是否运行');
updateConnectionStatus(false);
// 错误时也尝试重连
if (!manualDisconnect.value) {
reconnect();
}
};
} catch (error) {
debugLog('连接失败:', error);
emit('error', '连接失败,请重试');
// 连接失败也尝试重连
if (!manualDisconnect.value) {
reconnect();
}
}
};
// 断开WebSocket连接
const disconnect = () => {
if (ws.value) {
manualDisconnect.value = true;
cancelReconnect();
ws.value.close();
updateConnectionStatus(false);
debugLog('已断开连接');
}
};
// 发送消息
const send = (message) => {
if (!message || !ws.value || !isConnected.value) {
debugLog('发送消息失败: 连接未建立或消息为空');
return false;
}
try {
ws.value.send(message);
debugLog('消息发送成功:', message);
return true;
} catch (error) {
debugLog('消息发送失败:', error);
emit('error', '消息发送失败');
return false;
}
};
// 切换连接状态
const toggleConnection = () => {
if (isConnected.value) {
disconnect();
} else {
connect();
}
};
// 监听props.url的变化
watch(() => props.url, (newUrl) => {
if (newUrl && newUrl !== currentUrl.value) {
// 如果当前已连接,则先断开连接
if (isConnected.value) {
disconnect();
// 延迟重新连接,确保旧连接完全关闭
setTimeout(() => {
connect(newUrl);
}, 500);
} else {
// 如果当前未连接直接更新URL
currentUrl.value = newUrl;
}
}
});
// 组件挂载时如果设置了autoConnect为true则自动连接
if (props.autoConnect) {
connect();
}
// 组件卸载时清理资源
onUnmounted(() => {
disconnect();
});
// 暴露方法给父组件使用
defineExpose({
connect,
disconnect,
send,
toggleConnection,
isConnected,
currentUrl
});
</script>
<template>
<!-- 这个组件没有UI界面元素 -->
</template>