Vue3 + TS 企业级 WebSocket 封装实战:高可用、自动重连、心跳检测与业务解耦方案
一、 前言
在实时性要求极高的应用场景(如:消防接处警中心、实时监控大屏、即时通讯等)中,WebSocket 是不可或缺的核心技术。然而,在实际开发中,简单的 new WebSocket(url) 远不能达到生产环境的要求。
我们需要面对以下挑战:
- 稳定性:网络抖动、代理服务器超时导致的连接断开。
- 僵死连接:客户端显示已连接,但实际上已无法接收消息。
- 业务解耦:如何让多个页面/组件方便地监听不同的实时数据流。
- 重连策略:如何优雅地进行重连,既要保证及时性,又要避免对服务器造成冲击。
本文将结合生产代码,深度解析一个企业级的 WebSocket 封装方案。
二、 核心技术实现 (websocket.ts)
1. 设计模式:单例模式
为了确保整个应用中只维护一个长连接,我们采用了单例模式。这样无论在哪个组件中引用,操作的都是同一个连接对象。
typescript
class WebSocketService {
// ... 私有属性
}
// 导出单例
const wsService = new WebSocketService(server.value);
export default wsService;
2. 精准的双向心跳检测 (Heartbeat Mechanism)
为什么需要心跳检测?因为在长连接中,可能会出现"半打开连接"的情况:客户端认为连接正常,但中间网络设备(如防火墙、代理服务器)已将其切断。
我们的方案采用了 "主动探测 + 超时判定" 的双定时器策略:
(1) 双定时器逻辑
heartbeatTimer(定期任务) :类似于"脉搏"。每隔 30s 触发一次,负责向服务端发送一个ping消息。serverTimeoutTimer(安全监控) :类似于"哨兵"。每当发出ping之后,都会启动这个定时器。如果在预设的 15s 内没有收到服务器的任何回应,哨兵就会判定连接已死,强制执行重连。
(2) 为什么是"双向"?
虽然是由客户端发起的 ping,但它能验证双向通路:
- 上行通路 :客户端能成功发出
ping。 - 下行通路 :服务端收到
ping后返回pong,或者发送了业务数据。注意: 只要客户端收到服务器发来的任何消息(包括pong或业务数据),都会触发resetHeartbeatTimeout(),重置"哨兵"定时器。
(3) 代码深度实现
typescript
/**
* 启动心跳:开启定期发送逻辑
*/
private startHeartbeat(): void {
this.stopHeartbeat(); // 清理旧定时器,防止叠加
this.heartbeatTimer = setInterval(() => {
if (this.connected) {
this.sendHeartbeatPing();
}
}, this.options.heartbeatInterval);
// 首次连接成功后立即发送一次,建立即时感知
this.sendHeartbeatPing();
}
/**
* 发送 Ping:触发上行探测
*/
private sendHeartbeatPing(): void {
try {
this.ws.send('ping'); // 框架约定的纯文本心跳
this.resetHeartbeatTimeout(); // 开启下行监控
} catch (error) {
console.error('发送心跳失败:', error);
}
}
/**
* 重置超时:只要收到消息,就说明链路是通的
*/
private resetHeartbeatTimeout(): void {
// 每次收到消息(onmessage)时都会调用此方法
if (this.serverTimeoutTimer) {
clearTimeout(this.serverTimeoutTimer);
}
// 开启一个延迟任务,如果 heartbeatTimeout 时间内没被再次重置,则说明服务器失联
this.serverTimeoutTimer = setTimeout(() => {
console.warn('WebSocket 心跳响应超时,主动关闭连接以触发重连');
if (this.ws) {
this.ws.close(); // 触发 onclose 回调,进入 handleReconnect 逻辑
}
}, this.options.heartbeatTimeout);
}
3. 高鲁棒性的自动重连机制
重连机制是 WebSocket 长连接的"生命线"。在移动端网络切换、电梯信号屏蔽或服务器短暂重启等场景下,自动重连能显著提升用户体验。
(1) 触发时机
我们的封装在以下三种情况下会自动触发重连:
onclose事件:正常或异常的连接关闭。onerror事件:建立连接失败或通信过程中发生错误。- 心跳超时 :通过
serverTimeoutTimer判定连接已成为"僵尸连接"时,主动关闭连接从而触发重连。
(2) 指数退避算法 (Exponential Backoff)
为了防止在服务器宕机时,大量客户端同时高频请求导致"雪崩效应",我们引入了指数退避策略。
- 逻辑:每次重连失败,下一次尝试的延迟时间都会翻倍。
- 配置项 :
reconnectDelay: 初始延迟(2s)。maxReconnectDelay: 最大延迟上限(60s)。maxReconnectAttempts: 最大重连次数(设置为 -1 表示无限重连,确保系统最终能自愈)。
typescript
private handleReconnect(): void {
// 1. 状态锁:如果已连接或正在连接,则跳过
if (this.connected || (this.ws && this.ws.readyState === WebSocket.CONNECTING)) return;
// 2. 检查重连次数上限
if (this.options.maxReconnectAttempts === -1 || this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.reconnectAttempts++;
// 3. 计算延迟:delay * 2^(n-1) -> 2s, 4s, 8s, 16s...
const backoffDelay = Math.min(
this.options.reconnectDelay * Math.pow(2, Math.max(0, this.reconnectAttempts - 1)),
this.options.maxReconnectDelay
);
console.log(`WebSocket 将在 ${backoffDelay / 1000}s 后进行第 ${this.reconnectAttempts} 次重连`);
this.reconnectTimer = setTimeout(async () => {
try {
await this.connect(); // 尝试重新建立连接
} catch (error) {
// connect 内部已处理错误,此处只需记录
console.error('本次重连尝试失败');
}
}, backoffDelay);
} else {
ElMessage.error('网络连接已断开,请手动刷新页面');
}
}
(3) 状态复位
重连成功后,必须及时重置状态,以便下一次故障时重新开始计算延迟:
- 在
onopen回调中将reconnectAttempts清零。 - 清除现有的
reconnectTimer。
4. 环境感知:网络与可见性
- online 监听:当电脑从断网恢复联网时,立即尝试重连。
- visibilitychange 监听:当用户切回浏览器标签页时,主动检查连接状态。
typescript
private initGlobalListeners(): void {
window.addEventListener('online', () => {
this.reconnectAttempts = 0; // 重置次数,立即尝试
this.connect();
});
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && !this.connected) {
this.connect();
}
});
}
5. 消息分发机制 (Topic-based)
通过 Map<string, MessageHandler[]> 维护消息处理器。当后端发来不同 type 的消息时,自动分发给注册了该 type 的回调函数。
typescript
on(type: string, handler: MessageHandler): void {
if (!this.messageHandlers.has(type)) {
this.messageHandlers.set(type, []);
}
this.messageHandlers.get(type)?.push(handler);
}
三、 业务层实战应用 (index.vue)
在接处警中心页面中,我们需要实时接收:新告警通知、被接单同步、接单超时通知。
1. 挂载与订阅
在组件挂载时注册对应的消息处理器。
typescript
onMounted(async () => {
// 初始化基础数据
await getReceivingCenterPageList();
// 注册 WebSocket 消息处理器
wsService.on('alarmInfo', processAlarmData); // 新告警
wsService.on('acceptOrder', handleAcceptOrderMessage); // 订单已被接
wsService.on('alarmInfoTimeOut', handleAcceptOrderTimeoutMessage); // 订单超时
// 监听连接状态用于 UI 反馈
wsService.onConnectionChange(handleConnectionChange);
// 建立连接
wsService.connect().catch(e => console.error(e));
});
2. 实时数据更新逻辑
以新告警为例,收到消息后直接将数据插入响应式数组的头部,并加上 newFlag 配合 CSS 动画(如呼吸灯效果)。
typescript
const processAlarmData = (alarmData) => {
const data = JSON.parse(alarmData);
if (data.alarmLevel === 1) {
// 插入列表首位,触发前端动画
redAlarms.value.unshift({ ...data, newFlag: true });
redTotal.value++;
}
};
3. 资源清理
在组件销毁时注销回调,避免内存泄漏或逻辑重复执行。
typescript
onUnmounted(() => {
wsService.off('alarmInfo', processAlarmData);
wsService.off('acceptOrder', handleAcceptOrderMessage);
wsService.offConnectionChange(handleConnectionChange);
// 注意:单例模式下不宜直接调用 disconnect(),因为其他页面可能还在使用
});
四、 核心优势总结
- 高可用性:通过双向心跳和多重重连机制,最大程度保证连接在线。
- 鉴权优化 :使用
RefreshToken建立连接。由于 WebSocket 建立后难以像 HTTP 那样方便地刷新 Access Token,使用 Refresh Token 能显著提升长连接的稳定性。 - 零入侵业务:封装后的服务通过事件订阅工作,页面无需关注 WebSocket 的连接、关闭、重连等底层细节,只需关注数据处理。
- 性能友好:指数退避重连算法和单例设计模式,有效保护了客户端性能和服务器压力。
五、 代码参考
- 工具类位置 :
src/utils/websocket.ts
websocket源文件