From 532648ebd8a42bc85a3f666b03694c5ce63422da Mon Sep 17 00:00:00 2001 From: wzy-warehouse <18135009705@163.com> Date: Mon, 18 May 2026 17:27:34 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84websocket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.json | 3 +- src/types/websocket/WebSocketConfig.ts | 15 +++ src/types/websocket/WebSocketOptions.ts | 17 --- src/utils/request/websocket.ts | 155 ++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 src/types/websocket/WebSocketConfig.ts delete mode 100644 src/types/websocket/WebSocketOptions.ts create mode 100644 src/utils/request/websocket.ts diff --git a/src/config/config.json b/src/config/config.json index bc42eed..63a4daf 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -2,8 +2,7 @@ "backendBaseUrl": "__BACKEND_BASE_URL__", "apiBaseUrl": "/api", "noEncryptUrls": [ - "/crypto/sm2/public-key", - "/algorithm-api/rainfall/grid" + "/crypto/sm2/public-key" ], "tdMapToken": [ "fc6cb1139b8eed4f79439130eb34eb00", diff --git a/src/types/websocket/WebSocketConfig.ts b/src/types/websocket/WebSocketConfig.ts new file mode 100644 index 0000000..57d2728 --- /dev/null +++ b/src/types/websocket/WebSocketConfig.ts @@ -0,0 +1,15 @@ +/** + * WebSocket连接配置选项 + */ +export interface WebSocketConfig { + /** WebSocket路径 */ + url: string; + /** 自动重连间隔(毫秒,默认3000) */ + reconnectDelay?: number; + /** 最大重连次数(默认5,-1表示无限重连) */ + maxReconnectAttempts?: number; + /** 心跳间隔(毫秒,默认50000) */ + heartbeatIncoming?: number; + /** 连接超时时间(毫秒,默认30000) */ + heartbeatOutgoing?: number; +} diff --git a/src/types/websocket/WebSocketOptions.ts b/src/types/websocket/WebSocketOptions.ts deleted file mode 100644 index 3eab65d..0000000 --- a/src/types/websocket/WebSocketOptions.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * WebSocket连接配置选项 - */ -export interface WebSocketOptions { - /** WebSocket路径 */ - path?: string; - /** 是否启用加密(默认true) */ - enableEncrypt?: boolean; - /** 自动重连间隔(毫秒,默认5000) */ - reconnectInterval?: number; - /** 最大重连次数(默认10,-1表示无限重连) */ - maxReconnectAttempts?: number; - /** 心跳间隔(毫秒,默认30000) */ - heartbeatInterval?: number; - /** 连接超时时间(毫秒,默认10000) */ - connectionTimeout?: number; -} diff --git a/src/utils/request/websocket.ts b/src/utils/request/websocket.ts new file mode 100644 index 0000000..bf5e36b --- /dev/null +++ b/src/utils/request/websocket.ts @@ -0,0 +1,155 @@ +import type { WebSocketConfig } from '@/types/websocket/WebSocketConfig'; +import { Client, type IMessage } from '@stomp/stompjs'; +import SockJS from 'sockjs-client'; + +export class WebSocketService { + private stompClient: Client | null = null; + private connected = false; + private reconnectAttempts = 0; + private config: Required; + + // 存储各个主题的回调列表 + private subscribers: Map void>> = new Map(); + + // 连接状态回调 + onConnected?: () => void; + onDisconnected?: () => void; + onError?: (error: unknown) => void; + + constructor(config: WebSocketConfig) { + this.config = { + url: config.url, + reconnectDelay: config.reconnectDelay || 3000, + maxReconnectAttempts: config.maxReconnectAttempts || 5, + heartbeatIncoming: config.heartbeatIncoming || 30000, + heartbeatOutgoing: config.heartbeatOutgoing || 30000, + }; + } + + /** + * 连接 WebSocket + */ + connect() { + if (this.connected) { + return; + } + + this.stompClient = new Client({ + webSocketFactory: () => new SockJS(this.config.url), + reconnectDelay: this.config.reconnectDelay, + heartbeatIncoming: this.config.heartbeatIncoming, + heartbeatOutgoing: this.config.heartbeatOutgoing, + }); + + // 连接成功回调 + this.stompClient.onConnect = () => { + this.connected = true; + this.reconnectAttempts = 0; + this.onConnected?.(); + }; + + // 断开连接回调 + this.stompClient.onDisconnect = () => { + this.connected = false; + this.onDisconnected?.(); + }; + + // 错误回调 + this.stompClient.onStompError = (frame) => { + console.error('STOMP 错误:', frame.headers['message']); + console.error('详细错误:', frame.body); + this.onError?.(frame); + }; + + try { + this.stompClient.activate(); + } catch (error) { + console.error('WebSocket 连接失败:', error); + this.handleError(error); + } + } + + /** + * 订阅某个主题,并注册回调 + */ + subscribe(topic: string, callback: (data: T) => void) { + if (!this.stompClient || !this.connected) { + console.error('WebSocket 未连接'); + return; + } + + // 如果还没有该主题的订阅者集合,则创建一个新的 + if (!this.subscribers.has(topic)) { + this.subscribers.set(topic, new Set()); + } + + // 添加当前回调到订阅者集合 + this.subscribers.get(topic)!.add(callback as (data: unknown) => void); + + // 实际订阅 STOMP 主题 + this.stompClient.subscribe(topic, (message: IMessage) => { + try { + const data = JSON.parse(message.body); + // 调用所有注册在该主题上的回调 + this.subscribers.get(topic)?.forEach((cb) => cb(data)); + } catch (error) { + console.error(`解析消息失败 [${topic}]:`, error); + this.onError?.(error); + } + }); + } + + /** + * 发送消息到指定目的地 + */ + send(destination: string, body: unknown) { + if (!this.stompClient || !this.connected) { + console.error('WebSocket 未连接'); + return; + } + + this.stompClient.publish({ + destination, + body: JSON.stringify(body), + }); + } + + /** + * 断开连接 + */ + disconnect() { + if (this.stompClient) { + this.stompClient.deactivate(); + this.stompClient = null; + this.connected = false; + this.subscribers.clear(); + } + } + + /** + * 检查连接状态 + */ + isConnected(): boolean { + return this.connected; + } + + /** + * 处理错误并重连 + */ + private handleError(error: unknown) { + this.reconnectAttempts++; + console.error( + `连接失败 (尝试 ${this.reconnectAttempts}/${this.config.maxReconnectAttempts}):`, + error + ); + + if (this.reconnectAttempts < this.config.maxReconnectAttempts) { + setTimeout(() => { + this.connect(); + }, this.config.reconnectDelay * this.reconnectAttempts); + } else { + console.error('达到最大重连次数,请检查网络连接'); + this.onError?.(error); + } + } +}