useSocket.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import { ref, reactive, onUnmounted, nextTick } from "vue";
  2. import { onHide } from "@dcloudio/uni-app";
  3. /**
  4. * useWebSocket - 封装适用于 uni-app App 平台的 WebSocket Hook
  5. * 支持自动重连、心跳检测、事件监听、消息记录等
  6. *
  7. * @param {string} url WebSocket 服务器地址(如 wss://example.com/ws)
  8. * @param {object} options 可选配置项
  9. * @returns {object} 提供连接状态、消息、发送、断开等方法
  10. */
  11. export default function useWebSocket(url, options = {}) {
  12. // 默认配置项
  13. const defaultOptions = {
  14. protocols: [], // 子协议
  15. autoConnect: true, // 是否自动连接
  16. reconnect: true, // 是否断线重连
  17. reconnectInterval: 3000, // 重连间隔(ms)
  18. maxReconnectAttempts: 3, // 最大重连次数
  19. heartbeat: true, // 是否启用心跳
  20. heartbeatInterval: 30000, // 心跳间隔(ms)
  21. heartbeatMessage: "ping", // 心跳消息内容
  22. ...options,
  23. };
  24. // 响应式状态
  25. const socketTask = ref(null); // WebSocket 实例
  26. const isConnected = ref(false); // 当前是否已连接
  27. const isConnecting = ref(false); // 当前是否正在连接
  28. const reconnectCount = ref(0); // 当前重连次数
  29. const lastMessage = ref(null); // 最后一条接收到的消息
  30. const messageHistory = ref([]); // 所有接收到的历史消息(上限100)
  31. // 事件监听器
  32. const listeners = reactive({
  33. open: [],
  34. message: [],
  35. error: [],
  36. close: [],
  37. });
  38. // 控制标志与定时器
  39. let reconnectTimer = null;
  40. let heartbeatTimer = null;
  41. let heartbeatTimeoutTimer = null;
  42. let isManuallyClosed = false; // 是否为手动断开(用于判断是否执行自动重连)
  43. /**
  44. * 创建 WebSocket 连接
  45. */
  46. const connect = () => {
  47. if (isConnected.value || isConnecting.value) {
  48. console.warn("WebSocket 已连接或正在连接中");
  49. return;
  50. }
  51. isConnecting.value = true;
  52. isManuallyClosed = false;
  53. // 发起连接
  54. socketTask.value = uni.connectSocket({
  55. url,
  56. protocols: defaultOptions.protocols,
  57. success: () => {
  58. console.log("WebSocket 连接请求已发出");
  59. nextTick(() => {
  60. setupListeners();
  61. });
  62. },
  63. fail: (err) => {
  64. isConnecting.value = false;
  65. console.error("WebSocket 连接请求失败:", err);
  66. handleError(err);
  67. },
  68. });
  69. };
  70. /**
  71. * 绑定 socket 事件监听器
  72. */
  73. const setupListeners = () => {
  74. console.log("socketTask.value", socketTask.value);
  75. if (!socketTask.value) return;
  76. socketTask.value.onOpen((res) => {
  77. console.log("WebSocket 已连接");
  78. isConnected.value = true;
  79. isConnecting.value = false;
  80. reconnectCount.value = 0;
  81. // 启动心跳机制
  82. if (defaultOptions.heartbeat) {
  83. startHeartbeat();
  84. }
  85. listeners.open.forEach((cb) => cb(res));
  86. });
  87. socketTask.value.onMessage((res) => {
  88. const message = {
  89. data: res.data,
  90. timestamp: Date.now(),
  91. };
  92. lastMessage.value = message;
  93. messageHistory.value.push(message);
  94. // 限制消息历史上限为100条
  95. if (messageHistory.value.length > 100) {
  96. messageHistory.value.shift();
  97. }
  98. listeners.message.forEach((cb) => cb(message));
  99. });
  100. socketTask.value.onError((err) => {
  101. console.error("WebSocket 发生错误:", err);
  102. handleError(err);
  103. });
  104. socketTask.value.onClose((res) => {
  105. console.log("WebSocket 已关闭", res);
  106. isConnected.value = false;
  107. isConnecting.value = false;
  108. stopHeartbeat();
  109. listeners.close.forEach((cb) => cb(res));
  110. // 自动重连(仅当未手动断开时)
  111. if (
  112. !isManuallyClosed &&
  113. defaultOptions.reconnect &&
  114. reconnectCount.value < defaultOptions.maxReconnectAttempts
  115. ) {
  116. attemptReconnect();
  117. }
  118. });
  119. };
  120. /**
  121. * 主动断开连接
  122. */
  123. const disconnect = () => {
  124. isManuallyClosed = true;
  125. clearReconnectTimer();
  126. stopHeartbeat();
  127. if (socketTask.value) {
  128. socketTask.value.close({
  129. code: 1000,
  130. reason: "客户端主动断开",
  131. });
  132. }
  133. isConnected.value = false;
  134. isConnecting.value = false;
  135. reconnectCount.value = 0;
  136. socketTask.value = null;
  137. };
  138. /**
  139. * 发送消息
  140. * @param {string|object} data 发送的数据(支持对象自动转 JSON)
  141. * @returns {Promise}
  142. */
  143. const send = (data) => {
  144. // console.log('isConnected', isConnected.value)
  145. if (!isConnected.value || !socketTask.value) {
  146. return Promise.reject(new Error("WebSocket 未连接"));
  147. }
  148. return new Promise((resolve, reject) => {
  149. try {
  150. const msg = typeof data === "object" ? JSON.stringify(data) : data;
  151. socketTask.value.send({
  152. data: msg,
  153. success: resolve,
  154. fail: reject,
  155. });
  156. } catch (err) {
  157. console.error("发送数据格式错误:", err);
  158. reject(err);
  159. }
  160. });
  161. };
  162. /**
  163. * 尝试自动重连
  164. */
  165. const attemptReconnect = () => {
  166. if (reconnectCount.value >= defaultOptions.maxReconnectAttempts) {
  167. console.warn("已达到最大重连次数,停止重连");
  168. return;
  169. }
  170. reconnectCount.value++;
  171. console.log(`尝试第 ${reconnectCount.value} 次重连...`);
  172. reconnectTimer = setTimeout(() => {
  173. connect();
  174. }, defaultOptions.reconnectInterval);
  175. };
  176. /**
  177. * 清除重连定时器
  178. */
  179. const clearReconnectTimer = () => {
  180. if (reconnectTimer) {
  181. clearTimeout(reconnectTimer);
  182. reconnectTimer = null;
  183. }
  184. };
  185. /**
  186. * 启动心跳检测
  187. */
  188. const startHeartbeat = () => {
  189. stopHeartbeat();
  190. heartbeatTimer = setInterval(() => {
  191. if (!isConnected.value) return;
  192. send(defaultOptions.heartbeatMessage)
  193. .then(() => {
  194. clearTimeout(heartbeatTimeoutTimer);
  195. heartbeatTimeoutTimer = setTimeout(() => {
  196. console.warn("心跳无响应,断开连接");
  197. socketTask.value?.close();
  198. }, defaultOptions.heartbeatInterval + 2000);
  199. })
  200. .catch((err) => {
  201. console.error("心跳发送失败:", err);
  202. });
  203. }, defaultOptions.heartbeatInterval);
  204. };
  205. /**
  206. * 停止心跳定时器
  207. */
  208. const stopHeartbeat = () => {
  209. if (heartbeatTimer) {
  210. clearInterval(heartbeatTimer);
  211. heartbeatTimer = null;
  212. }
  213. if (heartbeatTimeoutTimer) {
  214. clearTimeout(heartbeatTimeoutTimer);
  215. heartbeatTimeoutTimer = null;
  216. }
  217. };
  218. /**
  219. * 分发错误事件
  220. * @param {any} error
  221. */
  222. const handleError = (error) => {
  223. listeners.error.forEach((cb) => cb(error));
  224. };
  225. /**
  226. * 注册事件监听器
  227. * @param {string} event 事件名 open/message/error/close
  228. * @param {Function} callback
  229. */
  230. const on = (event, callback) => {
  231. if (listeners[event]) {
  232. listeners[event].push(callback);
  233. }
  234. };
  235. /**
  236. * 移除事件监听器
  237. * @param {string} event
  238. * @param {Function} callback
  239. */
  240. const off = (event, callback) => {
  241. if (listeners[event]) {
  242. const index = listeners[event].indexOf(callback);
  243. if (index > -1) {
  244. listeners[event].splice(index, 1);
  245. }
  246. }
  247. };
  248. /**
  249. * 清除消息历史
  250. */
  251. const clearHistory = () => {
  252. messageHistory.value = [];
  253. lastMessage.value = null;
  254. };
  255. /**
  256. * 手动执行重连(带清理)
  257. */
  258. const reconnect = () => {
  259. disconnect();
  260. nextTick(() => {
  261. reconnectCount.value = 0;
  262. isManuallyClosed = false;
  263. connect();
  264. });
  265. };
  266. // 页面加载时自动连接
  267. if (defaultOptions.autoConnect) {
  268. connect();
  269. }
  270. // 页面离开时时断开连接
  271. // onHide(() => {
  272. // disconnect()
  273. // })
  274. // 对外暴露状态与操作方法
  275. return {
  276. isConnected,
  277. isConnecting,
  278. reconnectCount,
  279. lastMessage,
  280. messageHistory,
  281. connect,
  282. disconnect,
  283. send,
  284. reconnect,
  285. clearHistory,
  286. on,
  287. off,
  288. };
  289. }