| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- import { ref, reactive, onUnmounted, nextTick } from "vue";
- import { onHide } from "@dcloudio/uni-app";
- /**
- * useWebSocket - 封装适用于 uni-app App 平台的 WebSocket Hook
- * 支持自动重连、心跳检测、事件监听、消息记录等
- *
- * @param {string} url WebSocket 服务器地址(如 wss://example.com/ws)
- * @param {object} options 可选配置项
- * @returns {object} 提供连接状态、消息、发送、断开等方法
- */
- export default function useWebSocket(url, options = {}) {
- // 默认配置项
- const defaultOptions = {
- protocols: [], // 子协议
- autoConnect: true, // 是否自动连接
- reconnect: true, // 是否断线重连
- reconnectInterval: 3000, // 重连间隔(ms)
- maxReconnectAttempts: 3, // 最大重连次数
- heartbeat: true, // 是否启用心跳
- heartbeatInterval: 30000, // 心跳间隔(ms)
- heartbeatMessage: "ping", // 心跳消息内容
- ...options,
- };
- // 响应式状态
- const socketTask = ref(null); // WebSocket 实例
- const isConnected = ref(false); // 当前是否已连接
- const isConnecting = ref(false); // 当前是否正在连接
- const reconnectCount = ref(0); // 当前重连次数
- const lastMessage = ref(null); // 最后一条接收到的消息
- const messageHistory = ref([]); // 所有接收到的历史消息(上限100)
- // 事件监听器
- const listeners = reactive({
- open: [],
- message: [],
- error: [],
- close: [],
- });
- // 控制标志与定时器
- let reconnectTimer = null;
- let heartbeatTimer = null;
- let heartbeatTimeoutTimer = null;
- let isManuallyClosed = false; // 是否为手动断开(用于判断是否执行自动重连)
- /**
- * 创建 WebSocket 连接
- */
- const connect = () => {
- if (isConnected.value || isConnecting.value) {
- console.warn("WebSocket 已连接或正在连接中");
- return;
- }
- isConnecting.value = true;
- isManuallyClosed = false;
- // 发起连接
- socketTask.value = uni.connectSocket({
- url,
- protocols: defaultOptions.protocols,
- success: () => {
- console.log("WebSocket 连接请求已发出");
- nextTick(() => {
- setupListeners();
- });
- },
- fail: (err) => {
- isConnecting.value = false;
- console.error("WebSocket 连接请求失败:", err);
- handleError(err);
- },
- });
- };
- /**
- * 绑定 socket 事件监听器
- */
- const setupListeners = () => {
- console.log("socketTask.value", socketTask.value);
- if (!socketTask.value) return;
- socketTask.value.onOpen((res) => {
- console.log("WebSocket 已连接");
- isConnected.value = true;
- isConnecting.value = false;
- reconnectCount.value = 0;
- // 启动心跳机制
- if (defaultOptions.heartbeat) {
- startHeartbeat();
- }
- listeners.open.forEach((cb) => cb(res));
- });
- socketTask.value.onMessage((res) => {
- const message = {
- data: res.data,
- timestamp: Date.now(),
- };
- lastMessage.value = message;
- messageHistory.value.push(message);
- // 限制消息历史上限为100条
- if (messageHistory.value.length > 100) {
- messageHistory.value.shift();
- }
- listeners.message.forEach((cb) => cb(message));
- });
- socketTask.value.onError((err) => {
- console.error("WebSocket 发生错误:", err);
- handleError(err);
- });
- socketTask.value.onClose((res) => {
- console.log("WebSocket 已关闭", res);
- isConnected.value = false;
- isConnecting.value = false;
- stopHeartbeat();
- listeners.close.forEach((cb) => cb(res));
- // 自动重连(仅当未手动断开时)
- if (
- !isManuallyClosed &&
- defaultOptions.reconnect &&
- reconnectCount.value < defaultOptions.maxReconnectAttempts
- ) {
- attemptReconnect();
- }
- });
- };
- /**
- * 主动断开连接
- */
- const disconnect = () => {
- isManuallyClosed = true;
- clearReconnectTimer();
- stopHeartbeat();
- if (socketTask.value) {
- socketTask.value.close({
- code: 1000,
- reason: "客户端主动断开",
- });
- }
- isConnected.value = false;
- isConnecting.value = false;
- reconnectCount.value = 0;
- socketTask.value = null;
- };
- /**
- * 发送消息
- * @param {string|object} data 发送的数据(支持对象自动转 JSON)
- * @returns {Promise}
- */
- const send = (data) => {
- // console.log('isConnected', isConnected.value)
- if (!isConnected.value || !socketTask.value) {
- return Promise.reject(new Error("WebSocket 未连接"));
- }
- return new Promise((resolve, reject) => {
- try {
- const msg = typeof data === "object" ? JSON.stringify(data) : data;
- socketTask.value.send({
- data: msg,
- success: resolve,
- fail: reject,
- });
- } catch (err) {
- console.error("发送数据格式错误:", err);
- reject(err);
- }
- });
- };
- /**
- * 尝试自动重连
- */
- const attemptReconnect = () => {
- if (reconnectCount.value >= defaultOptions.maxReconnectAttempts) {
- console.warn("已达到最大重连次数,停止重连");
- return;
- }
- reconnectCount.value++;
- console.log(`尝试第 ${reconnectCount.value} 次重连...`);
- reconnectTimer = setTimeout(() => {
- connect();
- }, defaultOptions.reconnectInterval);
- };
- /**
- * 清除重连定时器
- */
- const clearReconnectTimer = () => {
- if (reconnectTimer) {
- clearTimeout(reconnectTimer);
- reconnectTimer = null;
- }
- };
- /**
- * 启动心跳检测
- */
- const startHeartbeat = () => {
- stopHeartbeat();
- heartbeatTimer = setInterval(() => {
- if (!isConnected.value) return;
- send(defaultOptions.heartbeatMessage)
- .then(() => {
- clearTimeout(heartbeatTimeoutTimer);
- heartbeatTimeoutTimer = setTimeout(() => {
- console.warn("心跳无响应,断开连接");
- socketTask.value?.close();
- }, defaultOptions.heartbeatInterval + 2000);
- })
- .catch((err) => {
- console.error("心跳发送失败:", err);
- });
- }, defaultOptions.heartbeatInterval);
- };
- /**
- * 停止心跳定时器
- */
- const stopHeartbeat = () => {
- if (heartbeatTimer) {
- clearInterval(heartbeatTimer);
- heartbeatTimer = null;
- }
- if (heartbeatTimeoutTimer) {
- clearTimeout(heartbeatTimeoutTimer);
- heartbeatTimeoutTimer = null;
- }
- };
- /**
- * 分发错误事件
- * @param {any} error
- */
- const handleError = (error) => {
- listeners.error.forEach((cb) => cb(error));
- };
- /**
- * 注册事件监听器
- * @param {string} event 事件名 open/message/error/close
- * @param {Function} callback
- */
- const on = (event, callback) => {
- if (listeners[event]) {
- listeners[event].push(callback);
- }
- };
- /**
- * 移除事件监听器
- * @param {string} event
- * @param {Function} callback
- */
- const off = (event, callback) => {
- if (listeners[event]) {
- const index = listeners[event].indexOf(callback);
- if (index > -1) {
- listeners[event].splice(index, 1);
- }
- }
- };
- /**
- * 清除消息历史
- */
- const clearHistory = () => {
- messageHistory.value = [];
- lastMessage.value = null;
- };
- /**
- * 手动执行重连(带清理)
- */
- const reconnect = () => {
- disconnect();
- nextTick(() => {
- reconnectCount.value = 0;
- isManuallyClosed = false;
- connect();
- });
- };
- // 页面加载时自动连接
- if (defaultOptions.autoConnect) {
- connect();
- }
- // 页面离开时时断开连接
- // onHide(() => {
- // disconnect()
- // })
- // 对外暴露状态与操作方法
- return {
- isConnected,
- isConnecting,
- reconnectCount,
- lastMessage,
- messageHistory,
- connect,
- disconnect,
- send,
- reconnect,
- clearHistory,
- on,
- off,
- };
- }
|