新增WebSocket组件

This commit is contained in:
zqm
2026-01-30 15:38:48 +08:00
parent 29d3b17657
commit c42a1fd5fd
9 changed files with 2181 additions and 5 deletions

View 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>

View File

@@ -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 }