WebSocket 工具类使用指南
📚 目录
概述
这是一个通用的 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: 有以下原因:
- 保持连接活跃:很多服务器或代理(如 Nginx)会断开长时间无数据的连接
- 及时发现断线:网络异常时,可能不会立即触发 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;