新增WebSocket组件
This commit is contained in:
491
Web/Vue/CubeLib/src/components/CubeWebSocket.vue
Normal file
491
Web/Vue/CubeLib/src/components/CubeWebSocket.vue
Normal file
@@ -0,0 +1,491 @@
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
wsUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
try {
|
||||
new URL(value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
autoConnect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
reconnect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
maxReconnectDelay: {
|
||||
type: Number,
|
||||
default: 30000,
|
||||
validator: (value) => value > 0
|
||||
},
|
||||
debugMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
heartbeatInterval: {
|
||||
type: Number,
|
||||
default: 30000,
|
||||
validator: (value) => value > 0
|
||||
},
|
||||
connectTimeout: {
|
||||
type: Number,
|
||||
default: 10000,
|
||||
validator: (value) => value > 0
|
||||
},
|
||||
maxQueueSize: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
validator: (value) => value > 0
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'connected',
|
||||
'disconnected',
|
||||
'error',
|
||||
'message',
|
||||
'status-changed',
|
||||
'connecting',
|
||||
'reconnecting',
|
||||
'message-sent',
|
||||
'message-queued',
|
||||
'message-failed'
|
||||
])
|
||||
|
||||
const ws = ref(null)
|
||||
const status = ref('disconnected')
|
||||
const reconnectAttempts = ref(0)
|
||||
const reconnectDelay = ref(1000)
|
||||
const messageQueue = ref([])
|
||||
const isOnline = ref(navigator.onLine)
|
||||
|
||||
const timers = {
|
||||
reconnect: null,
|
||||
heartbeat: null,
|
||||
connectTimeout: null
|
||||
}
|
||||
|
||||
const log = (...args) => {
|
||||
if (props.debugMode) {
|
||||
console.log('[CubeWebSocket]', ...args)
|
||||
}
|
||||
}
|
||||
|
||||
const logError = (...args) => {
|
||||
if (props.debugMode) {
|
||||
console.error('[CubeWebSocket]', ...args)
|
||||
}
|
||||
}
|
||||
|
||||
const buildBinaryMessage = (message) => {
|
||||
const jsonStr = JSON.stringify(message)
|
||||
const msgType = 0
|
||||
|
||||
const len = jsonStr.length
|
||||
const lenBytes = new ArrayBuffer(4)
|
||||
const lenView = new DataView(lenBytes)
|
||||
lenView.setUint32(0, len, true)
|
||||
|
||||
const extParamBytes = new ArrayBuffer(2)
|
||||
const extParamView = new DataView(extParamBytes)
|
||||
extParamView.setUint16(0, 0, true)
|
||||
|
||||
const msgBytes = new Uint8Array(1 + 2 + 4 + len)
|
||||
msgBytes[0] = msgType
|
||||
for (let i = 0; i < 2; i++) {
|
||||
msgBytes[1 + i] = new Uint8Array(extParamBytes)[i]
|
||||
}
|
||||
for (let i = 0; i < 4; i++) {
|
||||
msgBytes[3 + i] = new Uint8Array(lenBytes)[i]
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
msgBytes[7 + i] = jsonStr.charCodeAt(i)
|
||||
}
|
||||
|
||||
return msgBytes
|
||||
}
|
||||
|
||||
const connect = () => {
|
||||
const wsUrl = props.wsUrl || 'ws://localhost:8086/ws'
|
||||
|
||||
try {
|
||||
if (ws.value) {
|
||||
if (ws.value.readyState === WebSocket.CONNECTING) {
|
||||
log('WebSocket正在连接中,跳过本次连接')
|
||||
return
|
||||
}
|
||||
if (ws.value.readyState === WebSocket.OPEN) {
|
||||
log('WebSocket已连接,跳过本次连接')
|
||||
return
|
||||
}
|
||||
ws.value.close()
|
||||
}
|
||||
|
||||
status.value = 'connecting'
|
||||
emit('status-changed', 'connecting')
|
||||
emit('connecting')
|
||||
|
||||
ws.value = new WebSocket(wsUrl)
|
||||
|
||||
startConnectTimer()
|
||||
|
||||
ws.value.onopen = () => {
|
||||
stopConnectTimer()
|
||||
status.value = 'connected'
|
||||
emit('status-changed', 'connected')
|
||||
reconnectAttempts.value = 0
|
||||
reconnectDelay.value = 1000
|
||||
log('WebSocket连接成功')
|
||||
emit('connected')
|
||||
startHeartbeat()
|
||||
flushQueue()
|
||||
}
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
handleMessage(event)
|
||||
}
|
||||
|
||||
ws.value.onclose = (event) => {
|
||||
stopConnectTimer()
|
||||
stopHeartbeat()
|
||||
log('WebSocket连接关闭:', event.code, event.reason)
|
||||
emit('disconnected', event.code, event.reason)
|
||||
|
||||
if (!event.wasClean && props.reconnect) {
|
||||
status.value = 'reconnecting'
|
||||
emit('status-changed', 'reconnecting')
|
||||
emit('reconnecting')
|
||||
reconnect()
|
||||
} else {
|
||||
status.value = 'disconnected'
|
||||
emit('status-changed', 'disconnected')
|
||||
}
|
||||
}
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
stopConnectTimer()
|
||||
stopHeartbeat()
|
||||
console.error('[CubeWebSocket] WebSocket错误:', error)
|
||||
emit('error', error)
|
||||
|
||||
if (props.reconnect) {
|
||||
status.value = 'reconnecting'
|
||||
emit('status-changed', 'reconnecting')
|
||||
emit('reconnecting')
|
||||
reconnect()
|
||||
} else {
|
||||
status.value = 'error'
|
||||
emit('status-changed', 'error')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
stopConnectTimer()
|
||||
console.error('[CubeWebSocket] WebSocket连接失败:', error)
|
||||
status.value = 'error'
|
||||
emit('status-changed', 'error')
|
||||
emit('error', error)
|
||||
|
||||
if (props.reconnect) {
|
||||
reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
cancelReconnect()
|
||||
stopHeartbeat()
|
||||
stopConnectTimer()
|
||||
|
||||
if (ws.value) {
|
||||
ws.value.close()
|
||||
ws.value = null
|
||||
}
|
||||
|
||||
status.value = 'disconnected'
|
||||
emit('status-changed', 'disconnected')
|
||||
}
|
||||
|
||||
const reconnect = () => {
|
||||
if (ws.value && ws.value.readyState === WebSocket.CONNECTING) {
|
||||
log('WebSocket正在连接中,跳过本次重连')
|
||||
return
|
||||
}
|
||||
|
||||
clearTimer('reconnect')
|
||||
|
||||
const delay = Math.min(reconnectDelay.value, props.maxReconnectDelay)
|
||||
|
||||
log(`WebSocket准备重连,延迟: ${delay}ms`)
|
||||
status.value = 'reconnecting'
|
||||
emit('status-changed', 'reconnecting')
|
||||
emit('reconnecting')
|
||||
|
||||
timers.reconnect = setTimeout(() => {
|
||||
reconnectAttempts.value++
|
||||
log(`WebSocket开始重连,尝试次数: ${reconnectAttempts.value}`)
|
||||
connect()
|
||||
|
||||
if (reconnectDelay.value < props.maxReconnectDelay) {
|
||||
reconnectDelay.value *= 2
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
const send = (type, data = {}) => {
|
||||
const message = { version: 1, type, data }
|
||||
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
const msgBytes = buildBinaryMessage(message)
|
||||
ws.value.send(msgBytes)
|
||||
log('发送消息:', message)
|
||||
emit('message-sent', message)
|
||||
} catch (error) {
|
||||
logError('发送消息失败:', error)
|
||||
emit('message-failed', { message, reason: 'send_error', error })
|
||||
}
|
||||
} else {
|
||||
if (messageQueue.value.length < props.maxQueueSize) {
|
||||
messageQueue.value.push({ type, data })
|
||||
log('消息已加入队列:', message)
|
||||
emit('message-queued', message)
|
||||
} else {
|
||||
logError('消息队列已满,丢弃消息:', message)
|
||||
emit('message-failed', { message, reason: 'queue_full' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const flushQueue = () => {
|
||||
while (messageQueue.value.length > 0) {
|
||||
const msg = messageQueue.value.shift()
|
||||
const message = { version: 1, type: msg.type, data: msg.data }
|
||||
|
||||
try {
|
||||
const msgBytes = buildBinaryMessage(message)
|
||||
ws.value.send(msgBytes)
|
||||
log('发送队列消息:', message)
|
||||
emit('message-sent', message)
|
||||
} catch (error) {
|
||||
logError('发送队列消息失败:', error)
|
||||
messageQueue.value.unshift(msg)
|
||||
emit('message-failed', { message, reason: 'send_error', error })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleMessage = (event) => {
|
||||
log('收到WebSocket消息,类型:', event.data instanceof Blob ? 'Blob' : typeof event.data)
|
||||
|
||||
if (event.data instanceof Blob) {
|
||||
event.data.arrayBuffer().then(buffer => {
|
||||
const data = new Uint8Array(buffer)
|
||||
log('WebSocket消息长度:', data.length)
|
||||
|
||||
let offset = 0
|
||||
|
||||
try {
|
||||
while (offset < data.length) {
|
||||
if (offset + 7 > data.length) {
|
||||
log('WebSocket消息不完整:数据不足读取消息头,需要7字节,剩余:', data.length - offset)
|
||||
break
|
||||
}
|
||||
|
||||
const msgType = data[offset]
|
||||
const extParam = new DataView(buffer, offset + 1, 2).getUint16(0, true)
|
||||
const len = new DataView(buffer, offset + 3, 4).getUint32(0, true)
|
||||
|
||||
log('消息头解析结果:', { msgType, extParam, len })
|
||||
|
||||
offset += 7
|
||||
|
||||
if (offset + len > data.length) {
|
||||
log('WebSocket消息不完整:数据不足读取消息内容,需要', len, '字节,剩余:', data.length - offset)
|
||||
break
|
||||
}
|
||||
|
||||
const content = new Uint8Array(buffer, offset, len)
|
||||
offset += len
|
||||
|
||||
if (msgType === 0) {
|
||||
const jsonStr = new TextDecoder().decode(content)
|
||||
log('解码后的WebSocket字符串消息:', jsonStr)
|
||||
|
||||
if (!jsonStr || jsonStr.trim() === '') {
|
||||
log('WebSocket消息为空')
|
||||
continue
|
||||
}
|
||||
|
||||
if (!jsonStr.trim().startsWith('{')) {
|
||||
log('WebSocket消息不是有效的JSON格式:', jsonStr)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const message = JSON.parse(jsonStr)
|
||||
log('解析后的WebSocket消息:', message)
|
||||
emit('message', message)
|
||||
} catch (jsonError) {
|
||||
logError('JSON解析错误:', jsonError)
|
||||
}
|
||||
} else if (msgType === 1) {
|
||||
const jsonContent = content.slice(4)
|
||||
const jsonStr = new TextDecoder().decode(jsonContent)
|
||||
log('解码后的WebSocket二进制消息:', jsonStr)
|
||||
|
||||
if (!jsonStr || jsonStr.trim() === '') {
|
||||
log('WebSocket消息为空')
|
||||
continue
|
||||
}
|
||||
|
||||
if (!jsonStr.trim().startsWith('{')) {
|
||||
log('WebSocket消息不是有效的JSON格式:', jsonStr)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const message = JSON.parse(jsonStr)
|
||||
log('解析后的WebSocket消息:', message)
|
||||
emit('message', message)
|
||||
} catch (jsonError) {
|
||||
logError('JSON解析错误:', jsonError)
|
||||
}
|
||||
} else {
|
||||
log('未知的消息类型:', msgType)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logError('WebSocket消息处理异常:', error)
|
||||
}
|
||||
}).catch(error => {
|
||||
logError('WebSocket数据处理错误:', error)
|
||||
})
|
||||
} else {
|
||||
log('收到WebSocket文本消息:', event.data)
|
||||
try {
|
||||
const message = JSON.parse(event.data)
|
||||
log('解析后的WebSocket文本消息:', message)
|
||||
emit('message', message)
|
||||
} catch (error) {
|
||||
logError('WebSocket文本消息解析错误:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startHeartbeat = () => {
|
||||
stopHeartbeat()
|
||||
timers.heartbeat = setInterval(() => {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
send('ping', {})
|
||||
}
|
||||
}, props.heartbeatInterval)
|
||||
}
|
||||
|
||||
const stopHeartbeat = () => {
|
||||
if (timers.heartbeat) {
|
||||
clearInterval(timers.heartbeat)
|
||||
timers.heartbeat = null
|
||||
log('已停止心跳')
|
||||
}
|
||||
}
|
||||
|
||||
const startConnectTimer = () => {
|
||||
stopConnectTimer()
|
||||
timers.connectTimeout = setTimeout(() => {
|
||||
if (ws.value && ws.value.readyState === WebSocket.CONNECTING) {
|
||||
logError('连接超时')
|
||||
ws.value.close()
|
||||
emit('error', new Error('Connection timeout'))
|
||||
}
|
||||
}, props.connectTimeout)
|
||||
}
|
||||
|
||||
const stopConnectTimer = () => {
|
||||
if (timers.connectTimeout) {
|
||||
clearTimeout(timers.connectTimeout)
|
||||
timers.connectTimeout = null
|
||||
log('已停止连接超时定时器')
|
||||
}
|
||||
}
|
||||
|
||||
const clearTimer = (timerName) => {
|
||||
if (timers[timerName]) {
|
||||
clearTimeout(timers[timerName])
|
||||
timers[timerName] = null
|
||||
log(`已清理定时器: ${timerName}`)
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllTimers = () => {
|
||||
Object.keys(timers).forEach(timerName => {
|
||||
clearTimer(timerName)
|
||||
})
|
||||
log('已清理所有定时器')
|
||||
}
|
||||
|
||||
const cancelReconnect = () => {
|
||||
clearTimer('reconnect')
|
||||
reconnectAttempts.value = 0
|
||||
reconnectDelay.value = 1000
|
||||
status.value = 'disconnected'
|
||||
emit('status-changed', 'disconnected')
|
||||
log('WebSocket重连已取消')
|
||||
}
|
||||
|
||||
const handleOnline = () => {
|
||||
log('网络已恢复')
|
||||
isOnline.value = true
|
||||
|
||||
if (props.reconnect && status.value !== 'connected') {
|
||||
log('网络已恢复,开始重连WebSocket...')
|
||||
reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
const handleOffline = () => {
|
||||
log('网络已断开')
|
||||
isOnline.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoConnect) {
|
||||
connect()
|
||||
}
|
||||
window.addEventListener('online', handleOnline)
|
||||
window.addEventListener('offline', handleOffline)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnect()
|
||||
clearAllTimers()
|
||||
window.removeEventListener('online', handleOnline)
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
connect,
|
||||
disconnect,
|
||||
send,
|
||||
reconnect,
|
||||
getStatus: () => status.value,
|
||||
isConnected: () => status.value === 'connected',
|
||||
getQueueSize: () => messageQueue.value.length
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,7 +1,9 @@
|
||||
import CubeSplitter from './components/CubeSplitter.vue'
|
||||
import CubeWebSocket from './components/CubeWebSocket.vue'
|
||||
|
||||
const components = {
|
||||
CubeSplitter
|
||||
CubeSplitter,
|
||||
CubeWebSocket
|
||||
}
|
||||
|
||||
const install = (app: any) => {
|
||||
@@ -16,4 +18,4 @@ const CubeLib = {
|
||||
}
|
||||
|
||||
export default CubeLib
|
||||
export { CubeSplitter }
|
||||
export { CubeSplitter, CubeWebSocket }
|
||||
Reference in New Issue
Block a user