diff --git a/WebSocket增强服务技术设计文档.md b/WebSocket增强服务技术设计文档.md new file mode 100644 index 0000000..a4926f0 --- /dev/null +++ b/WebSocket增强服务技术设计文档.md @@ -0,0 +1,646 @@ +# WebSocket增强服务技术设计文档 + +## 概述 + +本文档详细解释了 `src/services/ws.ts` 的技术设计思路、架构选择和实现细节。这个文件实现了一个增强的WebSocket服务,在保持原有接口不变的前提下,添加了心跳检测、自动重连、错误处理等企业级功能。 + +## 设计目标 + +### 主要目标 + +1. **零侵入性**:业务代码无需修改,完全透明的功能增强 +2. **企业级稳定性**:心跳检测、自动重连、错误恢复 +3. **可配置性**:全局配置,易于调整和优化 +4. **类型安全**:完整的TypeScript类型支持 +5. **内存安全**:正确的资源管理,防止内存泄漏 + +### 兼容性目标 + +- 保持原有 `create(path): Promise` 接口不变 +- 返回标准WebSocket实例,支持所有原生API +- 业务代码中的 `ws.onmessage`, `ws.close()` 等调用完全兼容 + +## 架构设计 + +### 整体架构图 + +``` +┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ +│ 业务代码 │ │ EnhancedWebSocket │ │ 原生WebSocket │ +│ │ │ (包装器) │ │ │ +│ ws.onmessage = ... │───▶│ 事件拦截和过滤 │───▶│ 实际网络连接 │ +│ ws.send(data) │ │ 心跳检测逻辑 │ │ │ +│ ws.close() │ │ 重连管理 │ │ │ +└─────────────────────┘ └──────────────────────┘ └─────────────────────┘ + │ + ▼ + ┌──────────────────────┐ + │ WS_CONFIG │ + │ (全局配置) │ + │ - 心跳间隔 │ + │ - 重连策略 │ + │ - 超时设置 │ + └──────────────────────┘ +``` + +### 设计模式选择 + +#### 1. 包装器模式 (Wrapper Pattern) + +```typescript +class EnhancedWebSocket { + private ws: WebSocket; // 包装原生WebSocket +} +``` + +**为什么选择包装器而不是继承?** + +1. **继承的问题**: + + ```typescript + // 继承方式的问题 + class EnhancedWebSocket extends WebSocket { + constructor(url: string) { + super(url); // 连接立即开始,无法在事件处理器设置前进行拦截 + } + } + ``` + +2. **包装器的优势**: + ```typescript + // 包装器方式的优势 + class EnhancedWebSocket { + constructor(path: string, baseUrl: string) { + this.ws = new WebSocket(baseUrl + path); // 控制创建时机 + this.setupHandlers(); // 立即设置我们的处理器 + } + } + ``` + +#### 2. 代理模式 (Proxy Pattern) + +通过getter/setter拦截用户对事件处理器的设置: + +```typescript +get onmessage(): ((event: MessageEvent) => void) | null { + return this.userOnMessage; +} + +set onmessage(handler: ((event: MessageEvent) => void) | null) { + this.userOnMessage = handler; // 保存用户的处理器 + // 我们的处理器已经在构造时设置,会调用用户的处理器 +} +``` + +## 核心技术实现 + +### 1. Class 设计选择 + +#### 为什么使用 Class? + +```typescript +class EnhancedWebSocket { + // 私有状态管理 + private ws: WebSocket; + private path: string; + private heartbeatTimer?: NodeJS.Timeout; + // ... +} +``` + +**选择Class的原因:** + +1. **状态封装**:WebSocket连接需要管理多个状态(连接、定时器、配置等) +2. **方法绑定**:事件处理器需要访问实例状态,Class提供了自然的this绑定 +3. **生命周期管理**:连接的创建、维护、销毁有清晰的生命周期 +4. **类型安全**:TypeScript对Class有更好的类型推导和检查 + +**与函数式方案的对比:** + +```typescript +// 函数式方案的问题 +function createEnhancedWS(path: string) { + let heartbeatTimer: NodeJS.Timeout; + let reconnectTimer: NodeJS.Timeout; + // 需要大量闭包来管理状态,复杂度高 +} + +// Class方案的优势 +class EnhancedWebSocket { + private heartbeatTimer?: NodeJS.Timeout; // 清晰的状态管理 + private reconnectTimer?: NodeJS.Timeout; + // 方法可以直接访问状态 +} +``` + +### 2. Private 成员设计 + +#### 为什么大量使用 private? + +```typescript +class EnhancedWebSocket { + private ws: WebSocket; // 内部WebSocket实例 + private path: string; // 连接路径 + private heartbeatTimer?: NodeJS.Timeout; // 心跳定时器 + private reconnectTimer?: NodeJS.Timeout; // 重连定时器 + private reconnectAttempts: number = 0; // 重连次数 + private isManualClose: boolean = false; // 手动关闭标志 + private isHeartbeatTimeout: boolean = false; // 心跳超时标志 +} +``` + +**Private的重要性:** + +1. **封装原则**:防止外部直接访问和修改内部状态 +2. **API稳定性**:内部实现可以随时重构,不影响公共接口 +3. **状态一致性**:防止外部代码破坏内部状态的一致性 +4. **错误预防**:避免用户误用内部API导致的bug + +**示例对比:** + +```typescript +// 如果没有private,用户可能这样做 +const ws = new EnhancedWebSocket('/test'); +ws.heartbeatTimer = undefined; // 💥 破坏了心跳检测 +ws.reconnectAttempts = -1; // 💥 破坏了重连逻辑 + +// 有了private,这些操作被编译器阻止 +// ✅ 确保了内部状态的安全性 +``` + +### 3. Constructor 设计 + +#### 构造函数的关键作用 + +```typescript +constructor(path: string, baseUrl: string) { + this.path = path; + this.baseUrl = baseUrl; + this.ws = new WebSocket(baseUrl + path); // 创建实际连接 + this.setupHandlers(); // 立即设置事件处理器 +} +``` + +**设计要点:** + +1. **立即执行**:构造时立即创建连接和设置处理器 +2. **状态初始化**:确保所有私有状态都有正确的初始值 +3. **参数验证**:(可以添加)对输入参数进行验证 +4. **最小权限**:只接收必要的参数,其他配置使用全局配置 + +**为什么不延迟创建连接?** + +```typescript +// ❌ 错误方案:延迟创建 +constructor(path: string, baseUrl: string) { + this.path = path; + this.baseUrl = baseUrl; + // 不创建连接,等用户调用connect() +} + +// ✅ 正确方案:立即创建 +constructor(path: string, baseUrl: string) { + // 立即创建,因为原有接口期望构造后就有连接 + this.ws = new WebSocket(baseUrl + path); + this.setupHandlers(); +} +``` + +### 4. Getter/Setter 设计 + +#### 透明的属性代理 + +```typescript +// 只读属性的getter +get readyState(): number { + return this.ws.readyState; // 直接代理到内部WebSocket +} + +get url(): string { + return this.ws.url; +} + +// 可写属性的getter/setter +get binaryType(): BinaryType { + return this.ws.binaryType; +} + +set binaryType(value: BinaryType) { + this.ws.binaryType = value; +} +``` + +**为什么需要这些getter/setter?** + +1. **API兼容性**:用户期望能够访问标准WebSocket的所有属性 +2. **透明代理**:用户感觉在使用标准WebSocket,实际上是我们的增强版本 +3. **状态同步**:确保外部看到的状态与内部WebSocket状态一致 + +#### 事件处理器的特殊getter/setter + +```typescript +// 事件处理器的拦截 +get onmessage(): ((event: MessageEvent) => void) | null { + return this.userOnMessage; // 返回用户设置的处理器 +} + +set onmessage(handler: ((event: MessageEvent) => void) | null) { + this.userOnMessage = handler; // 保存用户的处理器 + // 我们的内部处理器会调用用户的处理器 +} +``` + +**关键设计思路:** + +1. **双层处理**:我们的处理器 + 用户的处理器 +2. **透明性**:用户感觉直接在设置WebSocket的事件处理器 +3. **控制权**:我们先处理(如过滤心跳),再传递给用户 + +### 5. 事件处理架构 + +#### 事件流设计 + +``` +WebSocket原生事件 → 我们的处理器 → 过滤/处理 → 用户的处理器 +``` + +#### 具体实现 + +```typescript +private setupHandlers(): void { + // 1. 设置我们的处理器 + this.ws.onmessage = (event) => { + const messageData = event.data; + + // 2. 我们先处理(心跳检测) + let isHeartbeatResponse = false; + if (typeof messageData === 'string' && messageData === WS_CONFIG.heartbeatResponseType) { + isHeartbeatResponse = true; + } + + if (isHeartbeatResponse) { + this.clearHeartbeatTimeout(); // 清除心跳超时 + return; // 不传递给用户 + } + + // 3. 传递给用户的处理器 + if (this.userOnMessage) { + this.userOnMessage(event); + } + }; +} +``` + +**设计优势:** + +1. **消息过滤**:自动过滤心跳消息,用户无感知 +2. **状态管理**:自动处理连接状态变化 +3. **错误恢复**:自动处理连接错误和重连 + +### 6. 定时器管理 + +#### 定时器生命周期管理 + +```typescript +class EnhancedWebSocket { + private heartbeatTimer?: NodeJS.Timeout; // 心跳发送定时器 + private heartbeatTimeoutTimer?: NodeJS.Timeout; // 心跳响应超时定时器 + private reconnectTimer?: NodeJS.Timeout; // 重连定时器 +} +``` + +**为什么需要三个定时器?** + +1. **heartbeatTimer**:定期发送心跳包 +2. **heartbeatTimeoutTimer**:检测心跳响应超时 +3. **reconnectTimer**:延迟重连 + +#### 定时器清理策略 + +```typescript +// 停止心跳检测 +private stopHeartbeat(): void { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = undefined; // 重置为undefined + } + this.clearHeartbeatTimeout(); // 同时清理超时检测 +} + +// 清除心跳响应超时检测 +private clearHeartbeatTimeout(): void { + if (this.heartbeatTimeoutTimer) { + clearTimeout(this.heartbeatTimeoutTimer); + this.heartbeatTimeoutTimer = undefined; // 重置为undefined + } +} +``` + +**内存安全保证:** + +1. **及时清理**:每次停止时都清理定时器 +2. **状态重置**:清理后设置为undefined +3. **多重清理**:在多个关键点都进行清理(连接关闭、手动关闭等) + +### 7. 状态标志设计 + +#### 关键状态标志 + +```typescript +private isManualClose: boolean = false; // 是否手动关闭 +private isHeartbeatTimeout: boolean = false; // 是否心跳超时 +private reconnectAttempts: number = 0; // 重连次数 +``` + +**为什么需要这些标志?** + +1. **区分关闭原因**:手动关闭 vs 异常断开 vs 心跳超时 +2. **重连决策**:根据不同原因决定是否重连 +3. **状态跟踪**:跟踪重连进度和次数 + +#### 状态转换逻辑 + +```typescript +// 心跳超时时 +private startHeartbeatTimeout(): void { + this.heartbeatTimeoutTimer = setTimeout(() => { + this.isHeartbeatTimeout = true; // 设置心跳超时标志 + this.ws.close(1000, 'Heartbeat timeout'); + }, WS_CONFIG.heartbeatTimeout); +} + +// 连接关闭时的决策 +this.ws.onclose = (event) => { + // 如果不是手动关闭,或者是心跳超时导致的关闭,则重连 + if (!this.isManualClose || this.isHeartbeatTimeout) { + this.scheduleReconnect(); + } + + this.isHeartbeatTimeout = false; // 重置标志 +}; +``` + +### 8. addEventListener/removeEventListener 实现 + +#### 为什么需要这些方法? + +```typescript +addEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | AddEventListenerOptions +): void { + this.ws.addEventListener(type, listener, options); +} + +removeEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | EventListenerOptions +): void { + this.ws.removeEventListener(type, listener, options); +} +``` + +**重要性:** + +1. **完整的API兼容性**:某些业务代码可能使用addEventListener而不是onXXX +2. **事件管理**:支持多个监听器 +3. **标准兼容**:遵循WebSocket标准API + +**类型安全:** + +- 使用泛型 `` 确保事件类型正确 +- listener参数的类型根据事件类型自动推导 + +### 9. 心跳检测机制 + +#### 心跳超时检测逻辑 + +```typescript +// 发送心跳时,只在没有超时检测时才设置新的 +this.heartbeatTimer = setInterval(() => { + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(WS_CONFIG.heartbeatMessage); + + if (!this.heartbeatTimeoutTimer) { + // 关键:避免重复设置 + this.startHeartbeatTimeout(); + } + } +}, WS_CONFIG.heartbeatInterval); +``` + +**设计要点:** + +1. **避免重复设置**:只有在没有超时检测时才设置新的 +2. **超时逻辑**:设定时间内没收到响应就断开连接 +3. **状态同步**:收到响应时清除超时检测 + +#### 心跳响应处理 + +```typescript +// 检查是否为心跳响应(支持字符串和JSON格式) +let isHeartbeatResponse = false; + +// 1. 检查简单字符串格式 +if (typeof messageData === 'string' && messageData === WS_CONFIG.heartbeatResponseType) { + isHeartbeatResponse = true; +} + +// 2. 检查JSON格式 +if (!isHeartbeatResponse && typeof messageData === 'string') { + try { + const data = JSON.parse(messageData); + if (data.type === WS_CONFIG.heartbeatResponseType) { + isHeartbeatResponse = true; + } + } catch (e) { + // JSON解析失败,不是JSON格式的心跳响应 + } +} +``` + +**兼容性设计**:支持两种心跳响应格式,适应不同的服务器实现。 + +### 10. 重连机制 + +#### 指数退避算法 + +```typescript +private scheduleReconnect(): void { + if (this.isManualClose || this.reconnectAttempts >= WS_CONFIG.maxReconnectAttempts) { + return; + } + + this.reconnectAttempts++; + + // 指数退避重连策略 + const delay = Math.min( + WS_CONFIG.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1), + WS_CONFIG.maxReconnectDelay + ); + + this.reconnectTimer = setTimeout(() => { + this.reconnect(); + }, delay); +} +``` + +**算法解释:** + +- 第1次重连:1000ms 后 +- 第2次重连:2000ms 后 +- 第3次重连:4000ms 后 +- 第4次重连:8000ms 后 +- 第5次重连:16000ms 后(受maxReconnectDelay限制,实际为30000ms) + +**设计考虑:** + +1. **指数退避**:避免对服务器造成压力 +2. **最大延迟限制**:防止延迟过长 +3. **次数限制**:避免无限重连 +4. **服务器友好**:给服务器恢复时间 + +### 11. 类型安全设计 + +#### 严格的类型定义 + +```typescript +// 事件处理器类型 +private userOnMessage: ((event: MessageEvent) => void) | null = null; +private userOnClose: ((event: CloseEvent) => void) | null = null; +private userOnError: ((event: Event) => void) | null = null; +private userOnOpen: ((event: Event) => void) | null = null; +``` + +**类型安全的好处:** + +1. **编译时检查**:在编译时捕获类型错误 +2. **IDE支持**:更好的自动补全和错误提示 +3. **重构安全**:类型系统确保重构的正确性 + +#### 泛型的使用 + +```typescript +addEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | AddEventListenerOptions +): void +``` + +**泛型的价值:** + +- `K extends keyof WebSocketEventMap`:确保事件类型只能是WebSocket支持的类型 +- `ev: WebSocketEventMap[K]`:根据事件类型自动推导事件对象类型 + +### 12. 资源管理 + +#### 完整的清理机制 + +```typescript +close(code?: number, reason?: string): void { + console.log(`手动关闭WebSocket: ${this.path}`); + this.isManualClose = true; + this.isHeartbeatTimeout = false; // 重置心跳超时标志 + this.stopHeartbeat(); // 清理心跳定时器 + this.clearReconnectTimer(); // 清理重连定时器 + this.ws.close(code, reason); // 关闭实际连接 +} +``` + +**资源清理的重要性:** + +1. **内存泄漏预防**:确保所有定时器都被清理 +2. **状态一致性**:重置所有状态标志 +3. **优雅关闭**:按正确顺序清理资源 + +## 配置设计 + +### 全局配置对象 + +```typescript +const WS_CONFIG = { + heartbeatInterval: 3000, // 心跳间隔 + heartbeatTimeout: 5000, // 心跳响应超时时间 + maxReconnectAttempts: 5, // 最大重连次数 + reconnectBaseDelay: 1000, // 重连基础延迟 + maxReconnectDelay: 30000, // 最大重连延迟 + heartbeatMessage: 'ping', // 心跳消息 + heartbeatResponseType: 'pong', // 心跳响应类型 +}; +``` + +**配置设计原则:** + +1. **集中管理**:所有配置在一个地方,易于维护 +2. **合理默认值**:开箱即用的配置 +3. **易于调整**:生产环境可以快速调整参数 +4. **文档化**:每个配置都有清晰的注释 + +## 接口兼容性 + +### 原有接口保持不变 + +```typescript +// 原有接口 +function create(path: string): Promise { + const baseUrl = import.meta.env.ENV_WEBSOCKET_BASE ?? ''; + const ws = new EnhancedWebSocket(path, baseUrl) as any; + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + ws.close(); + reject(new Error('WebSocket connection timeout')); + }, 10000); + + ws.addEventListener('open', () => { + clearTimeout(timeout); + resolve(ws); // 返回增强的WebSocket,但类型为WebSocket + }); + + ws.addEventListener('error', (e: any) => { + clearTimeout(timeout); + reject(e); + }); + }); +} +``` + +**兼容性保证:** + +1. **相同的函数签名**:`create(path: string): Promise` +2. **相同的返回类型**:返回Promise +3. **相同的使用方式**:业务代码无需任何修改 + +## 总结 + +### 技术选择总结 + +| 技术选择 | 原因 | 替代方案 | 为什么不选择替代方案 | +| ------------- | -------------------------------- | ------------ | ----------------------- | +| Class | 状态封装、方法绑定、生命周期管理 | 函数+闭包 | 复杂度高,类型支持差 | +| 包装器模式 | 控制创建时机、事件拦截 | 继承 | 无法在事件设置前拦截 | +| Private成员 | 封装、API稳定性、状态保护 | Public成员 | 容易被误用,状态不安全 | +| Getter/Setter | 透明代理、API兼容性 | 直接方法 | 不符合WebSocket API习惯 | +| 多定时器 | 职责分离、精确控制 | 单定时器 | 逻辑混乱,难以维护 | +| 状态标志 | 精确控制重连逻辑 | 仅依赖状态码 | WebSocket状态码限制多 | + +### 架构优势 + +1. **零侵入性**:业务代码完全无需修改 +2. **高可靠性**:多重保障确保连接稳定 +3. **高可维护性**:清晰的架构和完整的类型支持 +4. **高性能**:最小的性能开销 +5. **高扩展性**:易于添加新功能 + +### 最佳实践体现 + +1. **单一职责原则**:每个方法只负责一个功能 +2. **开闭原则**:对扩展开放,对修改封闭 +3. **依赖倒置原则**:依赖抽象(接口)而非具体实现 +4. **接口隔离原则**:用户只看到需要的接口 +5. **里氏替换原则**:增强版本完全可以替换原版本 + +这个实现展示了如何在保持向后兼容的同时,提供企业级的功能增强,是一个很好的渐进式增强的例子。 diff --git a/src/services/ws.ts b/src/services/ws.ts index a35036b..bb0c2d8 100644 --- a/src/services/ws.ts +++ b/src/services/ws.ts @@ -1,8 +1,328 @@ +// WebSocket全局配置 +const WS_CONFIG = { + heartbeatInterval: 30000, // 30秒心跳间隔 + heartbeatTimeout: 5000, // 心跳响应超时时间(5秒) + maxReconnectAttempts: 5, // 最大重连次数 + reconnectBaseDelay: 1000, // 重连基础延迟1秒 + maxReconnectDelay: 30000, // 最大重连延迟30秒 + heartbeatMessage: 'ping', // 心跳消息 + heartbeatResponseType: 'pong', // 心跳响应类型 +}; + +// 增强的WebSocket包装器 +class EnhancedWebSocket { + private ws: WebSocket; + private path: string; + private baseUrl: string; + private heartbeatTimer?: NodeJS.Timeout; + private heartbeatTimeoutTimer?: NodeJS.Timeout; + private reconnectTimer?: NodeJS.Timeout; + private reconnectAttempts: number = 0; + private isManualClose: boolean = false; + private isHeartbeatTimeout: boolean = false; + private userOnMessage: ((event: MessageEvent) => void) | null = null; + private userOnClose: ((event: CloseEvent) => void) | null = null; + private userOnError: ((event: Event) => void) | null = null; + private userOnOpen: ((event: Event) => void) | null = null; + + constructor(path: string, baseUrl: string) { + this.path = path; + this.baseUrl = baseUrl; + this.ws = new WebSocket(baseUrl + path); + this.setupHandlers(); + } + + // 设置事件处理器 + private setupHandlers(): void { + this.ws.onopen = (event) => { + console.log(`WebSocket连接已建立: ${this.path}`); + this.reconnectAttempts = 0; + this.clearReconnectTimer(); + this.startHeartbeat(); + + if (this.userOnOpen) { + this.userOnOpen(event); + } + }; + + this.ws.onmessage = (event) => { + const messageData = event.data; + + // 检查是否为心跳响应(支持字符串和JSON格式) + let isHeartbeatResponse = false; + + // 1. 检查是否为简单字符串心跳响应 + if (typeof messageData === 'string' && messageData === WS_CONFIG.heartbeatResponseType) { + isHeartbeatResponse = true; + } + + // 2. 检查是否为JSON格式心跳响应 + if (!isHeartbeatResponse && typeof messageData === 'string') { + try { + const data = JSON.parse(messageData); + if (data.type === WS_CONFIG.heartbeatResponseType) { + isHeartbeatResponse = true; + } + } catch (e) { + // JSON解析失败,不是JSON格式的心跳响应 + } + } + + if (isHeartbeatResponse) { + // 收到心跳响应,清除超时定时器 + this.clearHeartbeatTimeout(); + return; + } + + // 传递给业务代码 + if (this.userOnMessage) { + this.userOnMessage(event); + } + }; + + this.ws.onclose = (event) => { + console.log(`WebSocket连接关闭: ${this.path}`, event.code, event.reason); + this.stopHeartbeat(); + + // 先调用业务代码的关闭处理 + if (this.userOnClose) { + this.userOnClose(event); + } + + // 如果不是手动关闭,或者是心跳超时导致的关闭,则重连 + if (!this.isManualClose || this.isHeartbeatTimeout) { + this.scheduleReconnect(); + } + + // 重置心跳超时标志 + this.isHeartbeatTimeout = false; + }; + + this.ws.onerror = (event) => { + console.error(`WebSocket连接错误: ${this.path}`, event); + this.stopHeartbeat(); + + if (this.userOnError) { + this.userOnError(event); + } + }; + } + + // 开始心跳检测 + private startHeartbeat(): void { + this.stopHeartbeat(); + console.log(`开始心跳检测: ${this.path}, 间隔: ${WS_CONFIG.heartbeatInterval}ms`); + this.heartbeatTimer = setInterval(() => { + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(WS_CONFIG.heartbeatMessage); + + // 只有在没有进行超时检测时才设置新的超时检测 + if (!this.heartbeatTimeoutTimer) { + this.startHeartbeatTimeout(); + } + } + }, WS_CONFIG.heartbeatInterval); + } + + // 停止心跳检测 + private stopHeartbeat(): void { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = undefined; + } + this.clearHeartbeatTimeout(); + } + + // 开始心跳响应超时检测 + private startHeartbeatTimeout(): void { + // 不再自动清除,只在收到响应时清除 + this.heartbeatTimeoutTimer = setTimeout(() => { + console.log(`心跳响应超时: ${this.path}, ${WS_CONFIG.heartbeatTimeout}ms内未收到响应,主动断开连接`); + // 设置心跳超时标志,触发重连 + this.isHeartbeatTimeout = true; + this.ws.close(1000, 'Heartbeat timeout'); // 使用正常关闭状态码,通过标志来判断是否重连 + }, WS_CONFIG.heartbeatTimeout); + } + + // 清除心跳响应超时检测 + private clearHeartbeatTimeout(): void { + if (this.heartbeatTimeoutTimer) { + clearTimeout(this.heartbeatTimeoutTimer); + this.heartbeatTimeoutTimer = undefined; + } + } + + // 安排重连 + private scheduleReconnect(): void { + if (this.isManualClose || this.reconnectAttempts >= WS_CONFIG.maxReconnectAttempts) { + console.log(`停止重连: ${this.path}, 手动关闭: ${this.isManualClose}, 重连次数: ${this.reconnectAttempts}`); + return; + } + + this.reconnectAttempts++; + + // 指数退避重连策略 + const delay = Math.min( + WS_CONFIG.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1), + WS_CONFIG.maxReconnectDelay, + ); + + console.log( + `WebSocket将在${delay}ms后重连: ${this.path} (${this.reconnectAttempts}/${WS_CONFIG.maxReconnectAttempts})`, + ); + + this.reconnectTimer = setTimeout(() => { + this.reconnect(); + }, delay); + } + + // 重连逻辑 + private reconnect(): void { + if (this.isManualClose) return; + + console.log(`WebSocket重连尝试: ${this.path} (${this.reconnectAttempts}/${WS_CONFIG.maxReconnectAttempts})`); + + // 创建新的WebSocket连接 + this.ws = new WebSocket(this.baseUrl + this.path); + this.setupHandlers(); + } + + // 清理重连定时器 + private clearReconnectTimer(): void { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = undefined; + } + } + + // 公开的WebSocket属性和方法 + get readyState(): number { + return this.ws.readyState; + } + + get url(): string { + return this.ws.url; + } + + get protocol(): string { + return this.ws.protocol; + } + + get extensions(): string { + return this.ws.extensions; + } + + get bufferedAmount(): number { + return this.ws.bufferedAmount; + } + + get binaryType(): BinaryType { + return this.ws.binaryType; + } + + set binaryType(value: BinaryType) { + this.ws.binaryType = value; + } + + // 事件处理器属性 + get onopen(): ((event: Event) => void) | null { + return this.userOnOpen; + } + + set onopen(handler: ((event: Event) => void) | null) { + this.userOnOpen = handler; + } + + get onmessage(): ((event: MessageEvent) => void) | null { + return this.userOnMessage; + } + + set onmessage(handler: ((event: MessageEvent) => void) | null) { + this.userOnMessage = handler; + } + + get onclose(): ((event: CloseEvent) => void) | null { + return this.userOnClose; + } + + set onclose(handler: ((event: CloseEvent) => void) | null) { + this.userOnClose = handler; + } + + get onerror(): ((event: Event) => void) | null { + return this.userOnError; + } + + set onerror(handler: ((event: Event) => void) | null) { + this.userOnError = handler; + } + + // WebSocket方法 + send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void { + this.ws.send(data); + } + + close(code?: number, reason?: string): void { + console.log(`手动关闭WebSocket: ${this.path}`); + this.isManualClose = true; + this.isHeartbeatTimeout = false; // 手动关闭时重置心跳超时标志 + this.stopHeartbeat(); + this.clearReconnectTimer(); + this.ws.close(code, reason); + } + + addEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void { + this.ws.addEventListener(type, listener, options); + } + + removeEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void { + this.ws.removeEventListener(type, listener, options); + } + + dispatchEvent(event: Event): boolean { + return this.ws.dispatchEvent(event); + } + + // 常量 + static readonly CONNECTING = WebSocket.CONNECTING; + static readonly OPEN = WebSocket.OPEN; + static readonly CLOSING = WebSocket.CLOSING; + static readonly CLOSED = WebSocket.CLOSED; + + readonly CONNECTING = WebSocket.CONNECTING; + readonly OPEN = WebSocket.OPEN; + readonly CLOSING = WebSocket.CLOSING; + readonly CLOSED = WebSocket.CLOSED; +} + function create(path: string): Promise { - const ws = new WebSocket((import.meta.env.ENV_WEBSOCKET_BASE ?? '') + path); + const baseUrl = import.meta.env.ENV_WEBSOCKET_BASE ?? ''; + const ws = new EnhancedWebSocket(path, baseUrl) as any; + return new Promise((resolve, reject) => { - ws.onopen = () => resolve(ws); - ws.onerror = (e) => reject(e); + const timeout = setTimeout(() => { + ws.close(); + reject(new Error('WebSocket connection timeout')); + }, 10000); // 10秒连接超时 + + ws.addEventListener('open', () => { + clearTimeout(timeout); + resolve(ws); + }); + + ws.addEventListener('error', (e: any) => { + clearTimeout(timeout); + reject(e); + }); }); } + export default { create };