271 lines
7.0 KiB
JavaScript
271 lines
7.0 KiB
JavaScript
// src/hooks/useWebSocket.js
|
||
import { ref, onUnmounted, computed } from 'vue';
|
||
import { ProtocolCodec, MessageType } from './ProtocolCodec';
|
||
|
||
export default function useWebSocket(url, options = {}) {
|
||
// 使用模块级变量存储 SocketTask
|
||
let socketTask = null;
|
||
let reconnectTimer = null;
|
||
|
||
// 配置合并
|
||
const _defaultOptions = {
|
||
identity: {},
|
||
protocols: ['binary'],
|
||
reconnectDelay: 3000,
|
||
onMessage: () => {},
|
||
onAuthSuccess: () => {},
|
||
onClose: () => {},
|
||
...options
|
||
};
|
||
|
||
// 响应式状态
|
||
const state = ref({
|
||
isConnected: false,
|
||
isConnecting: false,
|
||
error: null,
|
||
clientId: '',
|
||
context: null
|
||
});
|
||
|
||
// 清理连接
|
||
const cleanupSocket = () => {
|
||
if (socketTask) {
|
||
try {
|
||
socketTask.close({ code: 1008, reason: '主动关闭' });
|
||
} catch (e) {
|
||
console.warn('关闭连接时出错:', e);
|
||
}
|
||
socketTask = null;
|
||
}
|
||
|
||
state.value = {
|
||
isConnected: false,
|
||
isConnecting: false,
|
||
error: null,
|
||
clientId: '',
|
||
context: null
|
||
};
|
||
|
||
if (reconnectTimer) {
|
||
clearTimeout(reconnectTimer);
|
||
reconnectTimer = null;
|
||
}
|
||
};
|
||
|
||
// 发送消息
|
||
const sendMessage = (data, isBinary = true) => {
|
||
return new Promise((resolve, reject) => {
|
||
if (!socketTask || !state.value.isConnected) {
|
||
const err = new Error('WebSocket 未连接');
|
||
state.value.error = err;
|
||
return reject(err);
|
||
}
|
||
|
||
const sendData = isBinary ? data : JSON.stringify(data);
|
||
|
||
socketTask.send({
|
||
data: sendData,
|
||
success: () => resolve(),
|
||
fail: (err) => {
|
||
console.error('消息发送失败:', err);
|
||
reject(new Error(`发送失败: ${err.errMsg || err.message}`));
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
// 发送身份校验包
|
||
const sendIdentityPacket = async () => {
|
||
const { user_id, token, name } = _defaultOptions.identity;
|
||
if (!user_id || !token) {
|
||
state.value.error = new Error('身份校验信息缺失(user_id/token)');
|
||
cleanupSocket();
|
||
return;
|
||
}
|
||
|
||
const identityPacket = ProtocolCodec.pack(MessageType.IDENTITY, {
|
||
user_id,
|
||
token,
|
||
name: name || `用户${user_id}`
|
||
});
|
||
|
||
try {
|
||
await sendMessage(identityPacket);
|
||
} catch (err) {
|
||
console.error('身份校验包发送失败', err);
|
||
cleanupSocket();
|
||
reconnectSocket();
|
||
}
|
||
};
|
||
|
||
// 处理服务端消息
|
||
const handleServerMessage = (res) => {
|
||
try {
|
||
const { data } = res;
|
||
let msgType, bodyData;
|
||
|
||
// 兼容不同数据格式
|
||
if (data instanceof ArrayBuffer) {
|
||
const uint8Array = new Uint8Array(data);
|
||
[msgType, _, bodyData] = ProtocolCodec.unpack(uint8Array);
|
||
} else if (typeof data === 'string') {
|
||
// 如果是字符串,可能是文本消息
|
||
console.warn('收到非二进制消息:', data);
|
||
return;
|
||
} else {
|
||
console.warn('未知的消息数据类型:', typeof data, data);
|
||
return;
|
||
}
|
||
|
||
const bodyStr = bodyData.toString('utf-8');
|
||
const parsedBody = bodyStr ? JSON.parse(bodyStr) : {};
|
||
|
||
if (msgType === MessageType.IDENTITY_RESP) {
|
||
state.value.context = parsedBody.data;
|
||
state.value.clientId = parsedBody.data?.client_id || '';
|
||
_defaultOptions.onAuthSuccess(parsedBody.data);
|
||
}
|
||
|
||
_defaultOptions.onMessage({
|
||
msgType,
|
||
data: parsedBody,
|
||
rawData: data
|
||
});
|
||
} catch (err) {
|
||
console.error('消息处理错误:', err);
|
||
state.value.error = new Error(`消息解析失败:${err.message}`);
|
||
_defaultOptions.onMessage({
|
||
msgType: 'ERROR',
|
||
data: { message: err.message },
|
||
rawData: res.data
|
||
});
|
||
}
|
||
};
|
||
|
||
// 初始化连接
|
||
const initSocket = () => {
|
||
if (state.value.isConnecting || state.value.isConnected) {
|
||
console.log('连接已存在或正在连接中,跳过重复连接');
|
||
return;
|
||
}
|
||
|
||
state.value.isConnecting = true;
|
||
state.value.error = null;
|
||
|
||
console.log('发起 WebSocket 连接:', url);
|
||
|
||
// 创建连接
|
||
socketTask = uni.connectSocket({
|
||
url,
|
||
protocols: _defaultOptions.protocols,
|
||
success: () => {
|
||
console.log('connectSocket API 调用成功');
|
||
},
|
||
fail: (err) => {
|
||
console.error('connectSocket API 调用失败:', err);
|
||
state.value.error = new Error(`连接创建失败:${err.errMsg || err.message}`);
|
||
state.value.isConnecting = false;
|
||
reconnectSocket();
|
||
}
|
||
});
|
||
socketTask.onOpen(() => {
|
||
console.log('xxxxxxxx');
|
||
})
|
||
// 检查实例是否有效
|
||
if (!socketTask) {
|
||
console.error('SocketTask 实例创建失败');
|
||
state.value.error = new Error('SocketTask 实例为空');
|
||
state.value.isConnecting = false;
|
||
reconnectSocket();
|
||
return;
|
||
}
|
||
|
||
// 绑定事件处理器
|
||
const openHandler = () => {
|
||
console.log('=== WebSocket 连接成功 ===');
|
||
state.value.isConnecting = false;
|
||
state.value.isConnected = true;
|
||
clearTimeout(reconnectTimer);
|
||
sendIdentityPacket();
|
||
};
|
||
|
||
const messageHandler = handleServerMessage;
|
||
|
||
const closeHandler = (res) => {
|
||
console.log('=== WebSocket 连接关闭 ===', res);
|
||
const closeInfo = {
|
||
code: res.code,
|
||
reason: res.reason,
|
||
isManual: res.code === 1008
|
||
};
|
||
_defaultOptions.onClose(closeInfo);
|
||
|
||
state.value.isConnected = false;
|
||
state.value.isConnecting = false;
|
||
|
||
if (!closeInfo.isManual && res.code !== 1000) {
|
||
reconnectSocket();
|
||
}
|
||
};
|
||
|
||
const errorHandler = (err) => {
|
||
console.log('=== WebSocket 连接错误 ===', err);
|
||
state.value.error = new Error(`连接错误:${err.errMsg || err.message}`);
|
||
state.value.isConnecting = false;
|
||
state.value.isConnected = false;
|
||
reconnectSocket();
|
||
};
|
||
|
||
// 绑定事件
|
||
socketTask.onOpen(openHandler);
|
||
socketTask.onMessage(messageHandler);
|
||
socketTask.onClose(closeHandler);
|
||
socketTask.onError(errorHandler);
|
||
|
||
// 存储事件处理器以便清理
|
||
socketTask._handlers = {
|
||
open: openHandler,
|
||
message: messageHandler,
|
||
close: closeHandler,
|
||
error: errorHandler
|
||
};
|
||
};
|
||
|
||
// 重连逻辑
|
||
const reconnectSocket = () => {
|
||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||
|
||
reconnectTimer = setTimeout(() => {
|
||
console.log(`尝试重连(延迟${_defaultOptions.reconnectDelay}ms)`);
|
||
initSocket();
|
||
}, _defaultOptions.reconnectDelay);
|
||
};
|
||
|
||
// 断开连接并停止重连
|
||
const disconnect = () => {
|
||
if (reconnectTimer) {
|
||
clearTimeout(reconnectTimer);
|
||
reconnectTimer = null;
|
||
}
|
||
cleanupSocket();
|
||
};
|
||
|
||
// 对外暴露的方法
|
||
const actions = {
|
||
connect: initSocket,
|
||
disconnect,
|
||
sendBinary: (data) => sendMessage(data, true),
|
||
sendJson: (data) => sendMessage(data, false),
|
||
reconnect: reconnectSocket
|
||
};
|
||
|
||
// 组件卸载清理
|
||
onUnmounted(() => {
|
||
disconnect();
|
||
});
|
||
|
||
return {
|
||
state: computed(() => ({ ...state.value })),
|
||
...actions
|
||
};
|
||
} |