WebSocket 工具类使用指南

WebSocket 工具类使用指南

📚 目录

  1. 概述
  2. 快速开始
  3. 工作流程详解
  4. [API 参考](#API 参考)
  5. 使用示例
  6. 完整流程串联

概述

这是一个通用的 WebSocket 客户端封装,可在 React、Vue 或原生 JavaScript 项目中使用。

核心特性

特性 说明
自动重连 断线后自动重连,使用指数退避策略(1s → 2s → 4s → 8s → 16s)
心跳保活 定时发送 ping,检测 pong 响应,超时自动重连
消息队列 断线时消息自动缓存,重连后自动发送
订阅管理 重连后自动恢复之前的所有订阅
Promise 支持 可通过 waitForConnection() 等待连接就绪
事件系统 发布/订阅模式,支持 on/off/once

快速开始

安装使用

javascript 复制代码
// 方式一:使用工厂函数(推荐)
import createWSClient from '@/utils/websocket';

const ws = createWSClient({
  url: 'wss://your-server.com/ws',
  debug: true // 开启调试日志
});

// 方式二:使用类
import { WSClient } from '@/utils/websocket';

const ws = new WSClient({
  url: 'wss://your-server.com/ws'
});

基本用法

javascript 复制代码
// 1. 创建实例
const ws = createWSClient({ url: 'wss://example.com/ws' });

// 2. 监听事件
ws.on('open', () => console.log('已连接'));
ws.on('message', data => console.log('收到消息:', data));
ws.on('close', () => console.log('已断开'));

// 3. 建立连接
ws.connect();

// 4. 发送消息
ws.send({ type: 'hello', data: 'world' });

// 5. 订阅主题
ws.subscribe('market.update');
ws.on('market.update', data => console.log('市场更新:', data));

工作流程详解

1️⃣ 连接建立流程

复制代码
用户调用 connect()
       ↓
检查 URL 是否有效
       ↓
检查是否已连接(防止重复)
       ↓
检查是否已永久失败
       ↓
处理 URL 协议(http→ws, https→wss)
       ↓
创建 WebSocket 实例
       ↓
绑定事件处理器

关键代码:

javascript 复制代码
connect(url) {
  // 如果传入新 URL,更新保存的地址
  if (url) this.url = url

  // 如果没有 URL,无法连接
  if (!this.url) {
    this.warn('未指定 WebSocket URL')
    return this
  }

  // 防止重复连接
  if (this.ws && (this.ws.readyState === WS_STATE.OPEN ||
                  this.ws.readyState === WS_STATE.CONNECTING)) {
    return this
  }

  // 处理 URL 协议(https 页面必须用 wss)
  const connectUrl = this.normalizeUrl(this.url)

  // 创建 WebSocket 实例
  this.ws = new WebSocket(connectUrl)

  // 绑定事件处理器
  this.ws.onopen = this.handleOpen.bind(this)
  this.ws.onmessage = this.handleMessage.bind(this)
  this.ws.onclose = this.handleClose.bind(this)
  this.ws.onerror = this.handleError.bind(this)
}

2️⃣ 连接成功处理流程

复制代码
触发 onopen
       ↓
重置重连计数和状态
       ↓
触发 'open' 事件
       ↓
解析等待连接的 Promise
       ↓
发送队列中的缓存消息
       ↓
恢复之前的订阅
       ↓
启动心跳机制

关键代码:

javascript 复制代码
handleOpen() {
  // 重置状态
  this.retryCount = 0
  this.isConnectionFailed = false
  this.missedHeartbeats = 0

  // 触发 open 事件
  this.emit('open')

  // 解析所有等待连接的 Promise
  this.connectPromiseResolvers.forEach(r => r.resolve())
  this.connectPromiseResolvers = []

  // 发送队列中的消息
  this.flushQueue()

  // 恢复之前的订阅
  this.restoreSubscriptions()

  // 启动心跳
  this.startHeartbeat()
}

3️⃣ 消息接收流程

复制代码
触发 onmessage
       ↓
解析 JSON 数据
       ↓
检查是否是心跳响应(pong)
       ├─ 是 → 重置心跳计数
       └─ 否 → 继续处理
       ↓
根据 message.type 触发对应事件
       ↓
根据 message.topic 触发对应事件
       ↓
触发通用 'message' 事件

关键代码:

javascript 复制代码
handleMessage(event) {
  // 解析消息数据
  let data = event.data

  // 尝试解析 JSON
  if (typeof data === 'string' && data.startsWith('{')) {
    try {
      data = JSON.parse(data)
    } catch (e) {
      // 保留原始字符串
    }
  }

  // 处理心跳响应
  if (data && data.type === 'pong') {
    this.missedHeartbeats = 0  // 重置心跳计数
    this.emit('pong', data)
    return
  }

  // 根据消息类型触发事件
  if (data && data.type) this.emit(data.type, data)

  // 根据消息主题触发事件
  if (data && data.topic) this.emit(data.topic, data)

  // 触发通用 message 事件
  this.emit('message', data)
}

4️⃣ 心跳保活机制

复制代码
连接成功后启动心跳定时器
       ↓
每隔 30 秒执行一次(可配置)
       ↓
增加 missedHeartbeats 计数
       ↓
检查是否超过阈值(默认 3 次)
       ├─ 是 → 主动断开连接,触发重连
       └─ 否 → 发送 ping 消息
       ↓
收到 pong → 重置 missedHeartbeats 为 0

关键代码:

javascript 复制代码
startHeartbeat() {
  // 先停止之前的心跳
  this.stopHeartbeat()

  // 重置心跳计数
  this.missedHeartbeats = 0

  // 创建心跳定时器
  this.heartbeatTimer = setInterval(() => {
    // 如果未连接,跳过
    if (!this.isConnected()) return

    // 增加未响应计数
    this.missedHeartbeats++

    // 如果连续多次未收到响应,主动断开并重连
    if (this.missedHeartbeats > this.heartbeatTimeout) {
      this.warn('心跳超时,主动断开')
      this.ws.close()
      return
    }

    // 发送心跳消息
    this.send({ type: 'ping', timestamp: Date.now() }, false)
  }, this.heartbeatInterval)
}

5️⃣ 断线重连流程

复制代码
触发 onclose
       ↓
触发 'close' 事件
       ↓
停止心跳
       ↓
检查是否允许重连
       ├─ 否 → 结束
       └─ 是 → 继续
       ↓
增加重连计数
       ↓
检查是否超过最大重试次数
       ├─ 是 → 标记永久失败,触发 connection_failed
       └─ 否 → 继续
       ↓
计算延迟时间(指数退避)
       ↓
延迟后调用 connect()

延迟计算公式:

复制代码
delay = min(baseDelay * 2^(retryCount-1), 16000)

第 1 次: 1000 * 2^0 = 1000ms  (1秒)
第 2 次: 1000 * 2^1 = 2000ms  (2秒)
第 3 次: 1000 * 2^2 = 4000ms  (4秒)
第 4 次: 1000 * 2^3 = 8000ms  (8秒)
第 5 次: 1000 * 2^4 = 16000ms (16秒,上限)

关键代码:

javascript 复制代码
handleClose(event) {
  // 触发 close 事件
  this.emit('close', { code: event.code, reason: event.reason })

  // 停止心跳
  this.stopHeartbeat()

  // 判断是否需要重连
  if (!this.shouldReconnect || this.isConnectionFailed) {
    return
  }

  // 增加重连计数
  this.retryCount++

  // 检查是否超过最大重试次数
  if (this.retryCount > this.maxRetries) {
    this.isConnectionFailed = true
    this.shouldReconnect = false
    this.emit('connection_failed', { message: 'WebSocket 服务不可用' })
    return
  }

  // 计算重连延迟(指数退避)
  const delay = Math.min(
    this.reconnectDelay * Math.pow(2, this.retryCount - 1),
    16000
  )

  // 延迟后重连
  setTimeout(() => this.connect(), delay)
}

6️⃣ 消息队列机制

复制代码
调用 send() 发送消息
       ↓
检查连接状态
       ├─ 已连接 → 直接发送
       └─ 未连接 → 加入消息队列
       ↓
连接成功后调用 flushQueue()
       ↓
逐个发送队列中的消息

关键代码:

javascript 复制代码
send(data, queue = true) {
  // 序列化数据
  const message = typeof data === 'string' ? data : JSON.stringify(data)

  // 如果连接已打开,直接发送
  if (this.isConnected()) {
    this.ws.send(message)
    return true
  }

  // 连接未打开,根据 queue 参数决定是否缓存
  if (queue && this.messageQueue.length < this.maxQueueSize) {
    this.messageQueue.push(message)
    return true
  }

  return false
}

flushQueue() {
  // 如果队列为空或未连接,跳过
  if (this.messageQueue.length === 0 || !this.isConnected()) {
    return
  }

  // 逐个发送
  while (this.messageQueue.length > 0) {
    const message = this.messageQueue.shift()
    this.ws.send(message)
  }
}

API 参考

构造函数配置

参数 类型 默认值 说明
url string '' WebSocket 服务器地址
reconnectDelay number 1000 重连基础延迟(毫秒)
maxRetries number 5 最大重试次数
heartbeatInterval number 30000 心跳间隔(毫秒)
heartbeatTimeout number 3 心跳超时次数
maxQueueSize number 100 消息队列最大长度
debug boolean false 是否开启调试日志

核心方法

方法 参数 返回值 说明
connect(url?) url: string WSClient 建立连接
waitForConnection(timeout?) timeout: number Promise 等待连接就绪
send(data, queue?) data: any, queue: boolean boolean 发送消息
subscribe(topic, metadata?) topic: string WSClient 订阅主题
unsubscribe(topic) topic: string WSClient 取消订阅
close(code?, reason?) code: number void 关闭连接
destroy() - void 销毁实例
reset() - void 重置连接状态

事件方法

方法 参数 返回值 说明
on(event, callback) event: string, callback: Function Function 注册监听器
once(event, callback) event: string, callback: Function Function 注册一次性监听器
off(event, callback) event: string, callback: Function void 移除监听器
offAll(event?) event?: string void 移除所有监听器

状态方法

方法 返回值 说明
isConnected() boolean 是否已连接
getState() string 获取连接状态
getSubscriptions() string[] 获取订阅列表
getStats() object 获取调试信息

内置事件

事件 触发时机 回调参数
open 连接成功 -
close 连接关闭 { code, reason }
error 连接错误 Event
message 收到任何消息 data
pong 收到心跳响应 { type: 'pong', ... }
connection_failed 连接永久失败 { message }

使用示例

Vue 3 中使用

javascript 复制代码
// composables/useWebSocket.js
import { ref, onMounted, onUnmounted } from 'vue';
import createWSClient from '@/utils/websocket';

export function useWebSocket(url) {
  const connected = ref(false);
  const messages = ref([]);

  const ws = createWSClient({ url, debug: true });

  // 监听连接状态
  ws.on('open', () => {
    connected.value = true;
  });

  ws.on('close', () => {
    connected.value = false;
  });

  // 监听消息
  ws.on('message', data => {
    messages.value.push(data);
  });

  onMounted(() => {
    ws.connect();
  });

  onUnmounted(() => {
    ws.destroy();
  });

  return {
    connected,
    messages,
    send: ws.send.bind(ws),
    subscribe: ws.subscribe.bind(ws)
  };
}

// 在组件中使用
// <script setup>
// const { connected, messages, send } = useWebSocket('wss://example.com/ws')
// </script>

React 中使用

javascript 复制代码
// hooks/useWebSocket.js
import { useState, useEffect, useRef, useCallback } from 'react';
import createWSClient from '@/utils/websocket';

export function useWebSocket(url) {
  const [connected, setConnected] = useState(false);
  const [messages, setMessages] = useState([]);
  const wsRef = useRef(null);

  useEffect(() => {
    // 创建实例
    const ws = createWSClient({ url, debug: true });
    wsRef.current = ws;

    // 监听事件
    ws.on('open', () => setConnected(true));
    ws.on('close', () => setConnected(false));
    ws.on('message', data => {
      setMessages(prev => [...prev, data]);
    });

    // 建立连接
    ws.connect();

    // 清理
    return () => ws.destroy();
  }, [url]);

  const send = useCallback(data => {
    wsRef.current?.send(data);
  }, []);

  return { connected, messages, send };
}

// 在组件中使用
// const { connected, messages, send } = useWebSocket('wss://example.com/ws')

等待连接就绪

javascript 复制代码
const ws = createWSClient({ url: 'wss://example.com/ws' });

async function init() {
  try {
    // 等待连接建立(超时 5 秒)
    await ws.waitForConnection(5000);

    console.log('连接成功!');

    // 订阅主题
    ws.subscribe('notifications');

    // 发送消息
    ws.send({ type: 'hello' });
  } catch (error) {
    console.error('连接失败:', error.message);
  }
}

ws.connect();
init();

带认证的订阅

javascript 复制代码
const ws = createWSClient({ url: 'wss://example.com/ws' });

// 订阅时附带 token
ws.subscribe('user.orders', { token: 'your-auth-token' });

// 重连后会自动带上 token 重新订阅

完整流程串联

下面是一个完整的使用场景,串联了所有核心流程:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        应用启动                                   │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  1. 创建实例                                                     │
│     const ws = createWSClient({ url, debug: true })              │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  2. 注册事件监听                                                 │
│     ws.on('open', ...) / ws.on('message', ...) / ws.on('close') │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  3. 建立连接                                                     │
│     ws.connect()                                                 │
│     → 创建 WebSocket 实例                                         │
│     → 绑定原生事件                                               │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  4. 连接成功 (handleOpen)                                        │
│     → 重置状态                                                   │
│     → 触发 'open' 事件                                           │
│     → 发送队列消息                                               │
│     → 恢复订阅                                                   │
│     → 启动心跳                                                   │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  5. 正常通信                                                     │
│     ← 收到消息 → 解析 → 触发对应事件                              │
│     → 发送消息 → 序列化 → WebSocket.send()                       │
│     ↔ 心跳ping/pong 保持连接活跃                                  │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  6. 连接断开 (handleClose)                                       │
│     → 触发 'close' 事件                                          │
│     → 停止心跳                                                   │
│     → 计算延迟(指数退避)                                         │
│     → 延迟后重连                                                 │
└─────────────────────────────────────────────────────────────────┘
                              ↓
                    返回步骤 3(自动重连)
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  7. 重连成功                                                     │
│     → 自动发送队列中的消息                                        │
│     → 自动恢复之前的订阅                                          │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  8. 应用退出                                                     │
│     ws.destroy()                                                │
│     → 关闭连接                                                   │
│     → 清理所有资源                                               │
└─────────────────────────────────────────────────────────────────┘

常见问题

Q: 为什么需要心跳机制?

A: 有以下原因:

  1. 保持连接活跃:很多服务器或代理(如 Nginx)会断开长时间无数据的连接
  2. 及时发现断线:网络异常时,可能不会立即触发 onclose,心跳可以主动检测

Q: 消息队列什么时候会满?

A: 当连接断开且队列满 100 条时,新消息会被丢弃。可以通过 maxQueueSize 配置调整。

Q: 如何处理认证过期?

A: 可以监听特定的消息类型,在收到认证失败消息时刷新 token 并重新订阅:

javascript 复制代码
ws.on('auth_failed', () => {
  const newToken = refreshToken();
  ws.unsubscribe('protected.topic');
  ws.subscribe('protected.topic', { token: newToken });
});

Q: 如何在组件卸载时正确清理?

A: 调用 destroy() 方法:

javascript 复制代码
// Vue
onUnmounted(() => ws.destroy());

// React
useEffect(() => {
  return () => ws.destroy();
}, []);

js 复制代码
/**
 * ============================================================================
 * 通用 WebSocket 客户端封装
 * ============================================================================
 *
 * 特性:
 * - 自动重连(指数退避策略)
 * - 心跳保活(带响应检测)
 * - 消息队列(断线时缓存,重连后发送)
 * - 订阅管理(重连后自动恢复)
 * - 事件系统(发布/订阅模式)
 * - Promise 支持(等待连接)
 * - 通用性(React/Vue/原生 JS 均可使用)
 */

/**
 * WebSocket 连接状态常量
 * 与原生 WebSocket.readyState 对应
 */
const WS_STATE = {
  CONNECTING: 0, // 连接中
  OPEN: 1, // 已连接
  CLOSING: 2, // 关闭中
  CLOSED: 3 // 已关闭
};

/**
 * WebSocket 客户端类
 */
class WSClient {
  /**
   * 构造函数
   * @param {Object} options - 配置选项
   * @param {string} options.url - WebSocket 服务器地址
   * @param {number} options.reconnectDelay - 重连基础延迟(毫秒),默认 1000
   * @param {number} options.maxRetries - 最大重试次数,默认 5
   * @param {number} options.heartbeatInterval - 心跳间隔(毫秒),默认 30000
   * @param {number} options.heartbeatTimeout - 心跳超时次数,默认 3
   * @param {number} options.maxQueueSize - 消息队列最大长度,默认 100
   * @param {boolean} options.debug - 是否开启调试日志,默认 false
   */
  constructor(options = {}) {
    // WebSocket 服务器地址
    this.url = options.url || '';

    // 原生 WebSocket 实例
    this.ws = null;

    // 事件监听器存储:{ eventName: [callback1, callback2, ...] }
    this.listeners = {};

    // 重连基础延迟时间(毫秒)
    this.reconnectDelay = options.reconnectDelay || 1000;

    // 是否允许自动重连
    this.shouldReconnect = true;

    // 心跳间隔时间(毫秒)
    this.heartbeatInterval = options.heartbeatInterval || 30000;

    // 心跳定时器引用
    this.heartbeatTimer = null;

    // 心跳超时计数(连续未收到 pong 的次数)
    this.missedHeartbeats = 0;

    // 心跳超时阈值(超过此次数断开重连)
    this.heartbeatTimeout = options.heartbeatTimeout || 3;

    // 订阅主题存储:Map<topic, metadata>
    this.subscriptions = new Map();

    // 重连尝试次数
    this.retryCount = 0;

    // 最大重试次数
    this.maxRetries = options.maxRetries || 5;

    // 消息队列(连接断开时缓存待发送消息)
    this.messageQueue = [];

    // 消息队列最大长度
    this.maxQueueSize = options.maxQueueSize || 100;

    // 连接是否已永久失败
    this.isConnectionFailed = false;

    // 调试模式
    this.debug = options.debug || false;

    // 连接成功的 Promise 回调存储
    this.connectPromiseResolvers = [];
  }

  /**
   * 输出调试日志
   * @param  {...any} args - 日志参数
   */
  log(...args) {
    // 只在调试模式下输出
    if (this.debug) {
      console.log('[WS]', ...args);
    }
  }

  /**
   * 输出警告日志
   * @param  {...any} args - 日志参数
   */
  warn(...args) {
    // 只在调试模式下输出
    if (this.debug) {
      console.warn('[WS]', ...args);
    }
  }

  /**
   * 获取当前连接状态
   * @returns {string} 状态名称
   */
  getState() {
    // 如果没有 WebSocket 实例,返回断开状态
    if (!this.ws) return 'DISCONNECTED';

    // 状态码映射为可读字符串
    const states = {
      [WS_STATE.CONNECTING]: 'CONNECTING',
      [WS_STATE.OPEN]: 'OPEN',
      [WS_STATE.CLOSING]: 'CLOSING',
      [WS_STATE.CLOSED]: 'CLOSED'
    };

    return states[this.ws.readyState] || 'UNKNOWN';
  }

  /**
   * 判断是否已连接
   * @returns {boolean}
   */
  isConnected() {
    return this.ws && this.ws.readyState === WS_STATE.OPEN;
  }

  /**
   * 根据页面协议自动选择 ws 或 wss
   * @param {string} url - 原始 URL
   * @returns {string} 处理后的 URL
   */
  normalizeUrl(url) {
    // 如果不在浏览器环境,直接返回
    if (typeof window === 'undefined' || !window.location) {
      return url;
    }

    // 如果页面是 https,将 ws 替换为 wss
    if (window.location.protocol === 'https:' && url.startsWith('ws://')) {
      return url.replace(/^ws:/, 'wss:');
    }

    return url;
  }

  /**
   * 建立 WebSocket 连接
   * @param {string} url - 可选,指定新的服务器地址
   * @returns {WSClient} 返回自身,支持链式调用
   */
  connect(url) {
    // 如果传入新 URL,更新保存的地址
    if (url) this.url = url;

    // 如果没有 URL,无法连接
    if (!this.url) {
      this.warn('未指定 WebSocket URL');
      return this;
    }

    // 防止重复连接
    if (this.ws && (this.ws.readyState === WS_STATE.OPEN || this.ws.readyState === WS_STATE.CONNECTING)) {
      return this;
    }

    // 如果已标记为永久失败,跳过连接
    if (this.isConnectionFailed) {
      this.warn('WebSocket 服务不可用,跳过连接');
      return this;
    }

    // 处理 URL 协议
    const connectUrl = this.normalizeUrl(this.url);

    // 创建 WebSocket 实例
    // 如果 URL 格式错误,WebSocket 构造函数会抛出异常
    // 使用 try-catch 是必要的,因为这是构造函数可能抛出的同步错误
    try {
      this.ws = new WebSocket(connectUrl);
    } catch (e) {
      this.warn('创建连接失败:', e.message);
      this.isConnectionFailed = true;
      this.emit('connection_failed', { error: e });
      return this;
    }

    this.log('正在连接:', connectUrl);

    // 绑定事件处理器
    this.ws.onopen = this.handleOpen.bind(this);
    this.ws.onmessage = this.handleMessage.bind(this);
    this.ws.onclose = this.handleClose.bind(this);
    this.ws.onerror = this.handleError.bind(this);

    return this;
  }

  /**
   * 等待连接建立
   * @param {number} timeout - 超时时间(毫秒),默认 5000
   * @returns {Promise<void>}
   */
  waitForConnection(timeout = 5000) {
    return new Promise((resolve, reject) => {
      // 如果已连接,直接返回
      if (this.isConnected()) {
        resolve();
        return;
      }

      // 如果已永久失败,直接拒绝
      if (this.isConnectionFailed) {
        reject(new Error('WebSocket 服务不可用'));
        return;
      }

      // 设置超时定时器
      const timer = setTimeout(() => {
        cleanup();
        reject(new Error('连接超时'));
      }, timeout);

      // 清理函数
      const cleanup = () => {
        clearTimeout(timer);
        // 从回调列表中移除
        const index = this.connectPromiseResolvers.indexOf(resolverObj);
        if (index > -1) this.connectPromiseResolvers.splice(index, 1);
      };

      // 成功回调
      const onSuccess = () => {
        cleanup();
        resolve();
      };

      // 失败回调
      const onFail = err => {
        cleanup();
        reject(err);
      };

      // 存储回调对象
      const resolverObj = { resolve: onSuccess, reject: onFail };
      this.connectPromiseResolvers.push(resolverObj);

      // 如果还没开始连接,触发连接
      if (!this.ws) {
        this.connect();
      }
    });
  }

  /**
   * 连接成功处理器
   */
  handleOpen() {
    this.log('连接成功');

    // 重置状态
    this.retryCount = 0;
    this.isConnectionFailed = false;
    this.missedHeartbeats = 0;

    // 触发 open 事件
    this.emit('open');

    // 解析所有等待连接的 Promise
    this.connectPromiseResolvers.forEach(r => r.resolve());
    this.connectPromiseResolvers = [];

    // 发送队列中的消息
    this.flushQueue();

    // 恢复之前的订阅
    this.restoreSubscriptions();

    // 启动心跳
    this.startHeartbeat();
  }

  /**
   * 消息接收处理器
   * @param {MessageEvent} event - 消息事件对象
   */
  handleMessage(event) {
    // 解析消息数据
    let data = event.data;

    // 尝试解析 JSON,如果失败保留原始字符串
    if (typeof data === 'string' && data.startsWith('{')) {
      try {
        data = JSON.parse(data);
      } catch (e) {
        // 保留原始字符串
      }
    }

    // 处理心跳响应
    if (data && data.type === 'pong') {
      this.missedHeartbeats = 0;
      this.emit('pong', data);
      return;
    }

    // 根据消息类型触发事件
    if (data && data.type) {
      this.emit(data.type, data);
    }

    // 根据消息主题触发事件
    if (data && data.topic) {
      this.emit(data.topic, data);
    }

    // 触发通用 message 事件
    this.emit('message', data);
  }

  /**
   * 连接关闭处理器
   * @param {CloseEvent} event - 关闭事件对象
   */
  handleClose(event) {
    this.log('连接关闭, code:', event.code, 'reason:', event.reason);

    // 触发 close 事件
    this.emit('close', { code: event.code, reason: event.reason });

    // 停止心跳
    this.stopHeartbeat();

    // 判断是否需要重连
    if (!this.shouldReconnect || this.isConnectionFailed) {
      return;
    }

    // 增加重连计数
    this.retryCount++;

    // 检查是否超过最大重试次数
    if (this.retryCount > this.maxRetries) {
      this.warn('重试次数超限,停止重连');
      this.isConnectionFailed = true;
      this.shouldReconnect = false;
      this.emit('connection_failed', { message: 'WebSocket 服务不可用' });

      // 拒绝所有等待的 Promise
      this.connectPromiseResolvers.forEach(r => r.reject(new Error('连接失败')));
      this.connectPromiseResolvers = [];
      return;
    }

    // 计算重连延迟(指数退避:1s, 2s, 4s, 8s, 16s)
    const delay = Math.min(this.reconnectDelay * Math.pow(2, this.retryCount - 1), 16000);

    this.log('将在', delay, 'ms 后重连,第', this.retryCount, '次尝试');

    // 延迟后重连
    setTimeout(() => this.connect(), delay);
  }

  /**
   * 连接错误处理器
   * @param {Event} event - 错误事件对象
   */
  handleError(event) {
    this.warn('连接错误');
    this.emit('error', event);
  }

  /**
   * 发送消息到服务器
   * @param {Object|string} data - 要发送的数据
   * @param {boolean} queue - 连接断开时是否加入队列,默认 true
   * @returns {boolean} 是否发送成功
   */
  send(data, queue = true) {
    // 序列化数据
    const message = typeof data === 'string' ? data : JSON.stringify(data);

    // 如果连接已打开,直接发送
    if (this.isConnected()) {
      this.ws.send(message);
      return true;
    }

    // 连接未打开,根据 queue 参数决定是否缓存
    if (queue && this.messageQueue.length < this.maxQueueSize) {
      this.messageQueue.push(message);
      this.log('消息已加入队列,队列长度:', this.messageQueue.length);
      return true;
    }

    this.warn('发送失败: 连接未打开');
    return false;
  }

  /**
   * 发送队列中的缓存消息
   */
  flushQueue() {
    // 如果队列为空或未连接,跳过
    if (this.messageQueue.length === 0 || !this.isConnected()) {
      return;
    }

    this.log('发送队列消息,数量:', this.messageQueue.length);

    // 逐个发送
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift();
      this.ws.send(message);
    }
  }

  /**
   * 订阅主题
   * @param {string} topic - 主题名称
   * @param {Object} metadata - 附加数据(如 token),会在重连时恢复
   * @returns {WSClient} 返回自身,支持链式调用
   */
  subscribe(topic, metadata = {}) {
    // 构造订阅消息
    const msg = { type: 'subscribe', topic, ...metadata };

    // 保存订阅信息(用于重连恢复)
    this.subscriptions.set(topic, metadata);

    // 发送订阅消息(如果未连接会自动加入队列)
    this.send(msg);

    return this;
  }

  /**
   * 取消订阅主题
   * @param {string} topic - 主题名称
   * @returns {WSClient} 返回自身,支持链式调用
   */
  unsubscribe(topic) {
    // 从订阅列表移除
    this.subscriptions.delete(topic);

    // 发送取消订阅消息
    if (this.isConnected()) {
      this.send({ type: 'unsubscribe', topic });
    }

    return this;
  }

  /**
   * 恢复所有订阅(重连后调用)
   */
  restoreSubscriptions() {
    // 如果没有订阅,跳过
    if (this.subscriptions.size === 0) return;

    this.log('恢复订阅,数量:', this.subscriptions.size);

    // 遍历所有订阅,重新发送订阅消息
    for (const [topic, metadata] of this.subscriptions.entries()) {
      const msg = { type: 'subscribe', topic, ...metadata };
      this.ws.send(JSON.stringify(msg));
    }
  }

  /**
   * 获取当前订阅的主题列表
   * @returns {string[]} 主题名称数组
   */
  getSubscriptions() {
    return Array.from(this.subscriptions.keys());
  }

  /**
   * 关闭连接
   * @param {number} code - 关闭状态码,默认 1000
   * @param {string} reason - 关闭原因
   */
  close(code = 1000, reason = 'Normal closure') {
    // 禁止自动重连
    this.shouldReconnect = false;

    // 停止心跳
    this.stopHeartbeat();

    // 关闭连接
    if (this.ws) {
      this.ws.close(code, reason);
      this.ws = null;
    }
  }

  /**
   * 销毁实例,清理所有资源
   */
  destroy() {
    this.close();
    this.listeners = {};
    this.subscriptions.clear();
    this.messageQueue = [];
    this.connectPromiseResolvers = [];
  }

  /**
   * 重置连接(用于重新启用已失败的连接)
   */
  reset() {
    this.close();
    this.isConnectionFailed = false;
    this.shouldReconnect = true;
    this.retryCount = 0;
    this.missedHeartbeats = 0;
    this.messageQueue = [];
  }

  /**
   * 注册事件监听器
   * @param {string} event - 事件名称
   * @param {Function} callback - 回调函数
   * @returns {Function} 返回取消监听的函数
   */
  on(event, callback) {
    // 如果该事件没有监听器数组,创建一个
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }

    // 添加回调到监听器数组
    this.listeners[event].push(callback);

    // 返回取消监听的函数
    return () => this.off(event, callback);
  }

  /**
   * 注册一次性事件监听器(触发后自动移除)
   * @param {string} event - 事件名称
   * @param {Function} callback - 回调函数
   * @returns {Function} 返回取消监听的函数
   */
  once(event, callback) {
    // 包装回调,执行后自动移除
    const wrapper = data => {
      this.off(event, wrapper);
      callback(data);
    };

    return this.on(event, wrapper);
  }

  /**
   * 移除事件监听器
   * @param {string} event - 事件名称
   * @param {Function} callback - 要移除的回调函数
   */
  off(event, callback) {
    // 如果该事件没有监听器,跳过
    if (!this.listeners[event]) return;

    // 过滤掉指定的回调
    this.listeners[event] = this.listeners[event].filter(fn => fn !== callback);
  }

  /**
   * 移除某事件的所有监听器
   * @param {string} event - 事件名称,不传则移除所有
   */
  offAll(event) {
    if (event) {
      delete this.listeners[event];
    } else {
      this.listeners = {};
    }
  }

  /**
   * 触发事件
   * @param {string} event - 事件名称
   * @param {*} data - 传递给回调的数据
   */
  emit(event, data) {
    // 获取该事件的所有监听器
    const callbacks = this.listeners[event] || [];

    // 遍历执行回调
    callbacks.forEach(callback => {
      // 使用 try-catch 防止单个回调错误影响其他回调
      try {
        callback(data);
      } catch (e) {
        console.error('[WS] 事件回调错误:', event, e);
      }
    });
  }

  /**
   * 启动心跳机制
   */
  startHeartbeat() {
    // 先停止之前的心跳
    this.stopHeartbeat();

    // 重置心跳计数
    this.missedHeartbeats = 0;

    // 创建心跳定时器
    this.heartbeatTimer = setInterval(() => {
      // 如果未连接,跳过
      if (!this.isConnected()) return;

      // 增加未响应计数
      this.missedHeartbeats++;

      // 如果连续多次未收到响应,主动断开并重连
      if (this.missedHeartbeats > this.heartbeatTimeout) {
        this.warn('心跳超时,主动断开');
        this.ws.close();
        return;
      }

      // 发送心跳消息
      this.send({ type: 'ping', timestamp: Date.now() }, false);
    }, this.heartbeatInterval);
  }

  /**
   * 停止心跳机制
   */
  stopHeartbeat() {
    // 如果定时器存在,清除它
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  /**
   * 获取调试信息
   * @returns {Object} 当前状态信息
   */
  getStats() {
    return {
      state: this.getState(),
      connected: this.isConnected(),
      retryCount: this.retryCount,
      missedHeartbeats: this.missedHeartbeats,
      subscriptions: this.getSubscriptions(),
      queueSize: this.messageQueue.length,
      url: this.url
    };
  }
}

/**
 * 创建 WebSocket 客户端实例
 * @param {Object} options - 配置选项
 * @returns {WSClient} WebSocket 客户端实例
 */
function createWSClient(options) {
  return new WSClient(options);
}

// 导出类和工厂函数
export { createWSClient, WSClient };

// 默认导出工厂函数
export default createWSClient;
相关推荐
意法半导体STM322 小时前
【官方原创】FDCAN数据段波特率增加后发送失败的问题分析 LAT1617
javascript·网络·stm32·单片机·嵌入式硬件·安全
我真会写代码2 小时前
WebSocket:告别轮询,实现Web实时通信 WebRTC:无需插件,实现浏览器端实时音视频通信
网络·websocket·网络协议·webrtc·实时音视频
only_Klein3 小时前
kubernetes Pod 通信过程演示
网络·kubernetes·tcpdump
以太浮标3 小时前
华为eNSP模拟器综合实验之- DHCP Option 43 解析
服务器·网络·华为·云计算
智算菩萨3 小时前
【网络工程师入门】DNS域名系统的深度解读与实践应用指南
网络·网络协议·系统架构
柒.梧.3 小时前
从零搭建SpringBoot+Vue+Netty+WebSocket+WebRTC视频聊天系统
vue.js·spring boot·websocket
弹简特3 小时前
【JavaSE-网络部分03】网络原理-泛泛介绍各个层次
java·开发语言·网络
数据安全科普王3 小时前
端口与进程的关系:网络服务是怎么“开门”的?
网络·其他
devmoon3 小时前
快速了解兼容 Ethereum 的 JSON-RPC 接口
开发语言·网络·rpc·json·区块链·智能合约·polkadot