简介

websocket在前端开发中,是一个必须掌握的技术!你可以不用,但必须掌握!

前几天,就遇到这样一个需求,要求界面的数据通过websocket实时推送,并且必须支持断网重连、自动心跳

自动心跳是定期向服务端发送小型数据包,如果一段时间内服务端没有收到心跳响应,系统可能会断开连接。

websokect的API非常简单

 1// 创建ws连接
 2const ws = new WebSocket('ws://localhost:8080/test');
 3ws.onopen = function() {  
 4  console.log('WebSocket 连接已经建立。');
 5  ws.send('Hello, server!');
 6};
 7ws.onmessage = function(event) {  
 8  console.log('收到服务器消息:',event.data);
 9};
10ws.onerror = function(event) {  
11  console.error('WebSocket 连接出现错误:'event);
12};
13ws.onclose = function() {   
14  console.log('WebSocket 连接已经关闭。');
15}

但是,要封装一个支持断网重连、自动心跳的websokect没有那么容易!

封装成功演示

核心优势

我们先看我封装的websokect,首先,最重要的,它的使用方法和官方Api完全一致!零学习成本,上手即用!

 1import WebSocketClient from "./WebSocketClient"
 2// 创建实例
 3const ws = new WebSocketClient('ws://localhost:3200');
 4// 连接
 5ws.connect()
 6// 同原生方法
 7ws.onclose(()=>{})
 8// 同原生方法
 9ws.onerror(()=>{})
10// 同原生方法
11ws.onmessage(()=>{  
12  // 同原生方法  ws.send("自定义发送的数据")
13})
14// 同原生方法
15ws.onopen(()=>{})
16// 关闭连接
17ws.close()

效果演示

后端服务创建

我们先使用node创建一个后端服务,安装ws库:

 1npm install ws

创建node index.js文件,引入WebSocket 服务器

 1const WebSocket = require("ws");
 2const wss = new WebSocket.Server({ port3200 });
 3console.log("服务运行在http://localhost:3200/");
 4wss.on("connection", (ws=> { 
 5  console.log("[服务器]:客官您来了~里边请");
 6  ws.send(`[websocket云端]您已经连接云端!数据推送中!`); 
 7  let index = 1;  
 8  const interval = setInterval(() => {
 9    ws.send(`[websocket]数据推送第${index}次`); 
10    index ++  
11  }, 1000 * 10); 
12  ws.on("close", () => { 
13    clearInterval(interval); 
14    // 清除定时器   
15    console.log("[服务器]:客官下次再来呢~");
16  });
17});

我们启动这个服务

 1node index.js

现在,我们在前端服务内进行连接测试

前端websokect测试

我们先写前端的相关逻辑

 1import { WebSocketClient } from '@/utils/dataDispatcher/WebSocketClient';
 2const ws = new WebSocketClient('ws://localhost:3200');
 3// 连接
 4ws.connect();
 5// 同原生方法
 6ws.onclose(() => {});
 7// 同原生方法
 8ws.onerror(() => {});
 9// 同原生方法
10ws.onmessage(() => {  
11  // 同原生方法   
12  ws.send('自定义发送的数据');
13});
14// 同原生方法
15ws.onopen(() => {});

启动项目,我们会发现控制台已经有了提示

心跳验证:

等待一段时间后,我们可以看到ws连接里,前端已经发送了多次心跳数据

服务端与客户端也一直在进行数据交互

断网重连验证:

可以看到,当我们断开服务端的时候,断网重连被自动触发。

技术路线

基本框架搭建

 1export class WebSocketClient {   
 2  // #socket链接   
 3  private url = '';   
 4// #socket实例    
 5private socketWebSocket | null = null;   
 6constructor(urlstring) {   
 7  super();    
 8  this.url = url;   
 9}      
10// >消息发送 
11public send(messagestring): void {     
12  if (this.socket && this.socket.readyState === WebSocket.OPEN) {     
13    this.socket.send(message);   
14else {          
15  console.error('[WebSocket] 未连接'); 
16}    
17}    
18// !初始化连接 
19public connect(): void {     
20  if (this.socket && this.socket.readyState === Web
21       Socket.OPEN) {     
22    return;     
23  }     
24  this.socket = new WebSocket(this.url);  
25// !websocket连接成功      
26this.socket.onopen = event => {   
27  console.log(`连接成功,等待服务端数据推送[onopen]...`);  
28};      
29this.socket.onmessage = event => { 
30};       
31this.socket.onclose = event => {  
32  console.log(`连接断开[onclose]...`); 
33};      
34this.socket.onerror = event => {      
35  console.log(`连接异常[onerror]...`);   
36};   
37}   
38// >关闭连接  
39public close(): void {     
40  if (this.socket) {       
41    this.socket.close();   
42    this.socket = null;   
43  }   
44}}

上述代码借助官方API实现了一个基本的 WebSocket 客户端,具有以下功能:

  • 初始化连接并处理各种 WebSocket 事件(打开、消息、关闭、错误)。

  • 发送消息到服务器。

  • 关闭连接。

现在,我们开始逐步完善代码,进行封装。

断网重连封装

 1export class WebSocketClient{
 2  // #socket链接
 3  private url = '';
 4// #socket实例
 5private socket: WebSocket | null = null;
 6// #重连次数
 7private reconnectAttempts = 0;
 8// #最大重连数
 9private maxReconnectAttempts = 5;
10// #重连间隔
11private reconnectInterval = 10000; // 10 seconds
12
13constructor(url: string) {
14  super();
15  this.url = url;
16}
17// >消息发送
18public send(message: string): void {
19  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
20    this.socket.send(message);
21  } else {
22  console.error('[WebSocket] 未连接');
23}
24}
25
26// !初始化连接
27public connect(): void {
28  if (this.reconnectAttempts === 0) {
29    console.log(`初始化连接中...`);
30  }
31  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
32    return;
33  }
34this.socket = new WebSocket(this.url);
35
36// !websocket连接成功
37this.socket.onopen = event => {
38  // 重置重连尝试成功连接
39  this.reconnectAttempts = 0;
40  console.log(`连接成功,等待服务端数据推送[onopen]...`);
41};
42
43this.socket.onmessage = event => {
44};
45
46this.socket.onclose = event => {
47  if (this.reconnectAttempts === 0) {
48    console.log(`连接断开[onclose]...`);
49  }
50  if (!this.stopWs) {
51    this.handleReconnect();
52  }
53};
54
55this.socket.onerror = event => {
56  if (this.reconnectAttempts === 0) {
57    console.log(`连接异常[onerror]...`);
58  }
59};
60}
61
62// > 断网重连逻辑
63private handleReconnect(): void {
64  if (this.reconnectAttempts < this.maxReconnectAttempts) {
65    this.reconnectAttempts++;
66    console.log(`尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
67    setTimeout(() => {
68      this.connect();
69    }, this.reconnectInterval);
70  } else {
71  console.log(`最大重连失败,终止重连: ${this.url}`);
72        }
73    }
74
75    // >关闭连接
76    public close(): void {
77        if (this.socket) {
78            this.socket.close();
79            this.socket = null;
80        }
81    }
82}

上述代码添加了自动断网重连的机制。其核心逻辑在于以下几个方面:

  1. 记录重连次数:通过 reconnectAttempts 属性记录当前已经尝试重连的次数。

  2. 设置最大重连次数:通过 maxReconnectAttempts 属性设置允许的最大重连次数。

  3. 重连逻辑:在 onclose 和 onerror 事件中调用重连处理函数 handleReconnect。

  4. 重连间隔:通过 reconnectInterval 属性设置每次重连的间隔时间,可以在每次重连时增加间隔以实现指数退避。

初始化连接并处理事件

在 connect 方法中,初始化 WebSocket 连接并为其设置事件处理函数。特别关注 onclose 和 onerror 事件,在连接关闭和出现错误时调用重连逻辑。

 1public connect(): void {
 2  if (this.reconnectAttempts === 0) {
 3    console.log(`初始化连接中...`);
 4  }
 5  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
 6    return;
 7  }
 8this.socket = new WebSocket(this.url);
 9
10this.socket.onopen = (event: Event) => {
11  this.reconnectAttempts = 0;
12  console.log(`连接成功,等待服务端数据推送[onopen]...`);
13};
14this.socket.onclose = (event: CloseEvent) => {
15  if (this.reconnectAttempts === 0) {
16    console.log(`连接断开[onclose]...`);
17  }
18  this.handleReconnect();
19};
20
21this.socket.onerror = (event: Event) => {
22  if (this.reconnectAttempts === 0) {
23    console.log(`连接异常[onerror]...`);
24  }
25  this.handleReconnect();
26};
27}

处理重连逻辑

在 handleReconnect 方法中,实现了实际的重连逻辑。该方法会递增 reconnectAttempts,检查是否达到最大重连次数,如果没有达到,则在指定的重连间隔后再次调用 connect 方法尝试重连。

 1private handleReconnect(): void {
 2  if (this.reconnectAttempts < this.maxReconnectAttempts) {
 3    this.reconnectAttempts++;
 4    console.log(`尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
 5    setTimeout(() => {
 6      this.connect();
 7    }, this.reconnectInterval * this.reconnectAttempts); // 重连间隔可以增加,例如指数退避
 8  } else {
 9  console.log(`最大重连失败,终止重连: ${this.url}`);
10    }
11}

关闭连接

在 close 方法中,手动关闭 WebSocket 连接并将 socket 设置为 null。

 1public close(): void {
 2  if (this.socket) {
 3    this.socket.close();
 4    this.socket = null;
 5  }
 6}

自动心跳封装

自动心跳(Automatic Heartbeat)是一种在网络通信中常用的机制,用于维持连接的活跃状态,检测连接是否仍然有效,并及时发现和处理连接断开或故障的情况。心跳机制通过定期发送“心跳”消息(通常是一个简单的 ping 或者 pong 消息)来确认连接双方的状态。

实现自动心跳的基本思路

  1. 发送心跳消息:在 WebSocket 连接建立后,启动一个定时器,定期发送心跳消息到服务器。

  2. 接收心跳响应:服务器收到心跳消息后返回响应,客户端接收到响应后重置定时器。

  3. 检测心跳超时:如果在指定时间内没有收到心跳响应,则认为连接断开,进行重连。

 1
 2export class WebSocketClient {
 3  // #socket链接
 4  private url = '';
 5// #socket实例
 6private socket: WebSocket | null = null;
 7// #重连次数
 8private reconnectAttempts = 0;
 9// #最大重连数
10private maxReconnectAttempts = 5;
11// #重连间隔
12private reconnectInterval = 10000; // 10 seconds
13// #发送心跳数据间隔
14private heartbeatInterval = 1000 * 30;
15// #计时器id
16private heartbeatTimer?: NodeJS.Timeout;
17// #彻底终止ws
18private stopWs = false;
19// *构造函数
20constructor(url: string) {
21  super();
22  this.url = url;
23}
24// >消息发送
25public send(message: string): void {
26  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
27    this.socket.send(message);
28  } else {
29  console.error('[WebSocket] 未连接');
30}
31}
32
33// !初始化连接
34public connect(): void {
35  if (this.reconnectAttempts === 0) {
36    console.log('WebSocket', `初始化连接中...`);
37  }
38  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
39    return;
40  }
41this.socket = new WebSocket(this.url);
42
43// !websocket连接成功
44this.socket.onopen = event => {
45  this.stopWs = false;
46  // 重置重连尝试成功连接
47  this.reconnectAttempts = 0;
48  // 在连接成功时停止当前的心跳检测并重新启动
49  this.startHeartbeat();
50  console.log(`连接成功,等待服务端数据推送[onopen]...`);
51};
52
53this.socket.onmessage = event => {
54  this.dispatchEvent('message', event);
55  this.startHeartbeat();
56};
57
58this.socket.onclose = event => {
59  if (this.reconnectAttempts === 0) {
60    console.log(`连接断开[onclose]...`);
61  }
62  if (!this.stopWs) {
63    this.handleReconnect();
64  }
65};
66
67this.socket.onerror = event => {
68  if (this.reconnectAttempts === 0) {
69    console.log(`连接异常[onerror]...`);
70  }
71  this.closeHeartbeat();
72};
73}
74
75// > 断网重连逻辑
76private handleReconnect(): void {
77  if (this.reconnectAttempts < this.maxReconnectAttempts) {
78    this.reconnectAttempts++;
79    console.log('WebSocket', `尝试重连...`);
80    setTimeout(() => {
81      this.connect();
82    }, this.reconnectInterval);
83  } else {
84  this.closeHeartbeat();
85console.log(`最大重连失败,终止重连: ${this.url}`);
86}
87}
88
89// >关闭连接
90public close(): void {
91  if (this.socket) {
92    this.stopWs = true;
93    this.socket.close();
94    this.socket = null;
95  }
96  this.closeHeartbeat();
97}
98
99// >开始心跳检测 -> 定时发送心跳消息
100private startHeartbeat(): void {
101  if (this.stopWs) return;
102  if (this.heartbeatTimer) {
103    this.closeHeartbeat();
104  }
105this.heartbeatTimer = setInterval(() => {
106  if (this.socket) {
107    this.socket.send(JSON.stringify({ type: 'heartBeat', data: {} }));
108    console.log('WebSocket', '送心跳数据...');
109  } else {
110    console.error('[WebSocket] 未连接');
111  }
112}, this.heartbeatInterval);
113}
114
115// >关闭心跳
116private closeHeartbeat(): void {
117  clearInterval(this.heartbeatTimer);
118  this.heartbeatTimer = undefined;
119}
120}

上述代码通过定时发送心跳消息来实现自动心跳机制,并结合断网重连逻辑来确保 WebSocket 连接的稳定性。

心跳机制的实现原理简析:

  • 在连接成功时启动心跳检测

在 connect() 方法中,当 WebSocket 连接成功(onopen 事件触发)时,调用 startHeartbeat() 方法。

 1this.socket.onopen = event => {
 2  this.stopWs = false;
 3  this.reconnectAttempts = 0;
 4  this.startHeartbeat();
 5  console.log(`连接成功,等待服务端数据推送[onopen]...`);
 6};
  • 定时发送心跳消息

startHeartbeat() 方法启动一个定时器,每隔 heartbeatInterval 时间(30秒)发送一次心跳消息。

 1private startHeartbeat(): void {
 2  if (this.stopWs) return;
 3  if (this.heartbeatTimer) {
 4    this.closeHeartbeat();
 5  }
 6this.heartbeatTimer = setInterval(() => {
 7  if (this.socket) {
 8    this.socket.send(JSON.stringify({ type: 'heartBeat', data: {} }));
 9    console.log('WebSocket', '发送心跳数据...');
10  } else {
11    console.error('[WebSocket] 未连接');
12  }
13}, this.heartbeatInterval);
14}
  • 停止心跳检测

closeHeartbeat() 方法用于停止心跳检测,清除定时器。

 1private closeHeartbeat(): void {
 2  clearInterval(this.heartbeatTimer);
 3    this.heartbeatTimer = undefined;
 4}
  • 在连接断开或发生错误时停止心跳检测

在 onclose 和 onerror 事件中调用 closeHeartbeat(),停止心跳检测。

 1this.socket.onclose = event => {
 2  if (this.reconnectAttempts === 0) {
 3    console.log(`连接断开[onclose]...`);
 4  }
 5  if (!this.stopWs) {
 6    this.handleReconnect();
 7  }
 8};
 9
10this.socket.onerror = event => {
11  if (this.reconnectAttempts === 0) {
12    console.log(`连接异常[onerror]...`);
13  }
14  this.closeHeartbeat();
15};

如何触发原生函数

现在,我们已经基本完成了功能的封装,那么,我们如何在外部调用原生的websokectApi呢?非常简单,借助几个自定义的生命周期函数即可!

 1import { EventDispatcher } from './dispatcher';
 2
 3export class WebSocketClient extends EventDispatcher {
 4
 5  //...
 6  constructor(url: string) {
 7    super();
 8    this.url = url;
 9  }
10  // >生命周期钩子
11  onopen(callBack: Function) {
12    this.addEventListener('open', callBack);
13  }
14  onmessage(callBack: Function) {
15    this.addEventListener('message', callBack);
16  }
17  onclose(callBack: Function) {
18    this.addEventListener('close', callBack);
19  }
20  onerror(callBack: Function) {
21    this.addEventListener('error', callBack);
22  }
23
24  // !初始化连接
25  public connect(): void {
26    // ...
27
28    // !websocket连接成功
29    this.socket.onopen = event => {
30      // ...
31      this.dispatchEvent('open', event);
32    };
33
34    this.socket.onmessage = event => {
35      this.dispatchEvent('message', event);
36      this.startHeartbeat();
37    };
38
39    this.socket.onclose = event => {
40      // ...
41      this.dispatchEvent('close', event);
42    };
43
44    this.socket.onerror = event => {
45      // ...
46      this.closeHeartbeat();
47      this.dispatchEvent('error', event);
48    };
49  }
50
51// >关闭连接
52public close(): void {
53  if (this.socket) {
54    this.stopWs = true;
55    this.socket.close();
56    this.socket = null;
57    this.removeEventListener('open');
58    this.removeEventListener('message');
59    this.removeEventListener('close');
60    this.removeEventListener('error');
61  }
62  this.closeHeartbeat();
63}
64
65// ...
66}

当原生的onclose、onopen方法触发时,会通过dispatchEvent触发相应的调度,进而触发通过addEventListener绑定的生命周期函数!

注意,这里的this.dispatchEvent方法,addEventListener方法都是通过类继承EventDispatcher方法获得的!

EventDispatcher源码如下:

 1export class EventDispatcher {
 2  private listeners: { [type: string]: Function[] } = {};
 3
 4protected addEventListener(type: string, listener: Function) {
 5  if (!this.listeners[type]) {
 6    this.listeners[type] = [];
 7  }
 8  if (this.listeners[type].indexOf(listener) === -1) {
 9    this.listeners[type].push(listener);
10  }
11}
12
13protected removeEventListener(type: string) {
14  this.listeners[type] = [];
15}
16
17protected dispatchEvent(type: string, data: any) {
18  const listenerArray = this.listeners[type] || [];
19  if (listenerArray.length === 0) return;
20  listenerArray.forEach(listener => {
21    listener.call(this, data);
22  });
23}
24}

关于EventDispatcher的实现原理,请参考博主的其他文章:

juejin.cn/post/735851…[1]

完整代码

ts版本

 1import { EventDispatcher } from './dispatcher';
 2
 3export class WebSocketClient extends EventDispatcher {
 4  // #socket链接
 5  private url = '';
 6  // #socket实例
 7  private socket: WebSocket | null = null;
 8  // #重连次数
 9  private reconnectAttempts = 0;
10  // #最大重连数
11  private maxReconnectAttempts = 5;
12  // #重连间隔
13  private reconnectInterval = 10000; // 10 seconds
14  // #发送心跳数据间隔
15  private heartbeatInterval = 1000 * 30;
16  // #计时器id
17  private heartbeatTimer?: NodeJS.Timeout;
18  // #彻底终止ws
19  private stopWs = false;
20  // *构造函数
21  constructor(url: string) {
22    super();
23    this.url = url;
24  }
25  // >生命周期钩子
26  onopen(callBack: Function) {
27    this.addEventListener('open', callBack);
28  }
29  onmessage(callBack: Function) {
30    this.addEventListener('message', callBack);
31  }
32  onclose(callBack: Function) {
33    this.addEventListener('close', callBack);
34  }
35  onerror(callBack: Function) {
36    this.addEventListener('error', callBack);
37  }
38  // >消息发送
39  public send(message: string): void {
40    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
41      this.socket.send(message);
42    } else {
43      console.error('[WebSocket] 未连接');
44    }
45  }
46
47  // !初始化连接
48  public connect(): void {
49    if (this.reconnectAttempts === 0) {
50      this.log('WebSocket', `初始化连接中...          ${this.url}`);
51    }
52    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
53      return;
54    }
55    this.socket = new WebSocket(this.url);
56
57    // !websocket连接成功
58    this.socket.onopen = event => {
59      this.stopWs = false;
60      // 重置重连尝试成功连接
61      this.reconnectAttempts = 0;
62      // 在连接成功时停止当前的心跳检测并重新启动
63      this.startHeartbeat();
64      this.log('WebSocket', `连接成功,等待服务端数据推送[onopen]...     ${this.url}`);
65      this.dispatchEvent('open', event);
66    };
67
68    this.socket.onmessage = event => {
69      this.dispatchEvent('message', event);
70      this.startHeartbeat();
71    };
72
73    this.socket.onclose = event => {
74      if (this.reconnectAttempts === 0) {
75        this.log('WebSocket', `连接断开[onclose]...    ${this.url}`);
76      }
77      if (!this.stopWs) {
78        this.handleReconnect();
79      }
80      this.dispatchEvent('close', event);
81    };
82
83    this.socket.onerror = event => {
84      if (this.reconnectAttempts === 0) {
85        this.log('WebSocket', `连接异常[onerror]...    ${this.url}`);
86      }
87      this.closeHeartbeat();
88      this.dispatchEvent('error', event);
89    };
90  }
91
92  // > 断网重连逻辑
93  private handleReconnect(): void {
94    if (this.reconnectAttempts < this.maxReconnectAttempts) {
95      this.reconnectAttempts++;
96      this.log('WebSocket', `尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})       ${this.url}`);
97      setTimeout(() => {
98        this.connect();
99      }, this.reconnectInterval);
100    } else {
101      this.closeHeartbeat();
102      this.log('WebSocket', `最大重连失败,终止重连: ${this.url}`);
103    }
104  }
105
106  // >关闭连接
107  public close(): void {
108    if (this.socket) {
109      this.stopWs = true;
110      this.socket.close();
111      this.socket = null;
112      this.removeEventListener('open');
113      this.removeEventListener('message');
114      this.removeEventListener('close');
115      this.removeEventListener('error');
116    }
117    this.closeHeartbeat();
118  }
119
120  // >开始心跳检测 -> 定时发送心跳消息
121  private startHeartbeat(): void {
122    if (this.stopWs) return;
123    if (this.heartbeatTimer) {
124      this.closeHeartbeat();
125    }
126    this.heartbeatTimer = setInterval(() => {
127      if (this.socket) {
128        this.socket.send(JSON.stringify({ type: 'heartBeat', data: {} }));
129        this.log('WebSocket', '送心跳数据...');
130      } else {
131        console.error('[WebSocket] 未连接');
132      }
133    }, this.heartbeatInterval);
134  }
135
136  // >关闭心跳
137  private closeHeartbeat(): void {
138    clearInterval(this.heartbeatTimer);
139    this.heartbeatTimer = undefined;
140  }
141}
142class Log {
143  private static console = true;
144  log(title: string, text: string) {
145    if (!Log.console) return;
146    if (import.meta.env.MODE === 'production') return;
147    const color = '#ff4d4f';
148    console.log(
149      `%c ${title} %c ${text} %c`,
150      `background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
151      `border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
152      'background:transparent'
153    );
154  }
155  closeConsole() {
156    Log.console = false;
157  }
158}
159export class EventDispatcher extends Log {
160  private listeners: { [type: string]: Function[] } = {};
161
162  protected addEventListener(type: string, listener: Function) {
163    if (!this.listeners[type]) {
164      this.listeners[type] = [];
165    }
166    if (this.listeners[type].indexOf(listener) === -1) {
167      this.listeners[type].push(listener);
168    }
169  }
170
171  protected removeEventListener(type: string) {
172    this.listeners[type] = [];
173  }
174
175  protected dispatchEvent(type: string, data: any) {
176    const listenerArray = this.listeners[type] || [];
177    if (listenerArray.length === 0) return;
178    listenerArray.forEach(listener => {
179      listener.call(this, data);
180    });
181  }
182}

js版本

 1export class WebSocketClient extends EventDispatcher {
 2  // #socket链接
 3  url = '';
 4  // #socket实例
 5  socket = null;
 6  // #重连次数
 7  reconnectAttempts = 0;
 8  // #最大重连数
 9  maxReconnectAttempts = 5;
10  // #重连间隔
11  reconnectInterval = 10000; // 10 seconds
12  // #发送心跳数据间隔
13  heartbeatInterval = 1000 * 30;
14  // #计时器id
15  heartbeatTimer = undefined;
16  // #彻底终止ws
17  stopWs = false;
18  // *构造函数
19  constructor(url) {
20    super();
21    this.url = url;
22  }
23  // >生命周期钩子
24  onopen(callBack) {
25    this.addEventListener('open', callBack);
26  }
27  onmessage(callBack) {
28    this.addEventListener('message', callBack);
29  }
30  onclose(callBack) {
31    this.addEventListener('close', callBack);
32  }
33  onerror(callBack) {
34    this.addEventListener('error', callBack);
35  }
36  // >消息发送
37  send(message) {
38    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
39      this.socket.send(message);
40    } else {
41      console.error('[WebSocket] 未连接');
42    }
43  }
44
45  // !初始化连接
46  connect() {
47    if (this.reconnectAttempts === 0) {
48      this.log('WebSocket', `初始化连接中...          ${this.url}`);
49    }
50    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
51      return;
52    }
53    this.socket = new WebSocket(this.url);
54
55    // !websocket连接成功
56    this.socket.onopen = event => {
57      this.stopWs = false;
58      // 重置重连尝试成功连接
59      this.reconnectAttempts = 0;
60      // 在连接成功时停止当前的心跳检测并重新启动
61      this.startHeartbeat();
62      this.log('WebSocket', `连接成功,等待服务端数据推送[onopen]...     ${this.url}`);
63      this.dispatchEvent('open', event);
64    };
65
66    this.socket.onmessage = event => {
67      this.dispatchEvent('message', event);
68      this.startHeartbeat();
69    };
70
71    this.socket.onclose = event => {
72      if (this.reconnectAttempts === 0) {
73        this.log('WebSocket', `连接断开[onclose]...    ${this.url}`);
74      }
75      if (!this.stopWs) {
76        this.handleReconnect();
77      }
78      this.dispatchEvent('close', event);
79    };
80
81    this.socket.onerror = event => {
82      if (this.reconnectAttempts === 0) {
83        this.log('WebSocket', `连接异常[onerror]...    ${this.url}`);
84      }
85      this.closeHeartbeat();
86      this.dispatchEvent('error', event);
87    };
88  }
89
90  // > 断网重连逻辑
91  handleReconnect() {
92    if (this.reconnectAttempts < this.maxReconnectAttempts) {
93      this.reconnectAttempts++;
94      this.log('WebSocket', `尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})       ${this.url}`);
95      setTimeout(() => {
96        this.connect();
97      }, this.reconnectInterval);
98    } else {
99      this.closeHeartbeat();
100      this.log('WebSocket', `最大重连失败,终止重连: ${this.url}`);
101    }
102  }
103
104  // >关闭连接
105  close() {
106    if (this.socket) {
107      this.stopWs = true;
108      this.socket.close();
109      this.socket = null;
110      this.removeEventListener('open');
111      this.removeEventListener('message');
112      this.removeEventListener('close');
113      this.removeEventListener('error');
114    }
115    this.closeHeartbeat();
116  }
117
118  // >开始心跳检测 -> 定时发送心跳消息
119  startHeartbeat() {
120    if (this.stopWs) return;
121    if (this.heartbeatTimer) {
122      this.closeHeartbeat();
123    }
124    this.heartbeatTimer = setInterval(() => {
125      if (this.socket) {
126        this.socket.send(JSON.stringify({ type: 'heartBeat', data: {} }));
127        this.log('WebSocket', '送心跳数据...');
128      } else {
129        console.error('[WebSocket] 未连接');
130      }
131    }, this.heartbeatInterval);
132  }
133
134  // >关闭心跳
135  closeHeartbeat() {
136    clearInterval(this.heartbeatTimer);
137    this.heartbeatTimer = undefined;
138  }
139}
140class Log {
141  static console = true;
142  log(title, text) {
143    if (!Log.console) return;
144    if (import.meta.env.MODE === 'production') return;
145    const color = '#ff4d4f';
146    console.log(
147      `%c ${title} %c ${text} %c`,
148      `background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
149      `border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
150      'background:transparent'
151    );
152  }
153  closeConsole() {
154    Log.console = false;
155  }
156}
157export class EventDispatcher extends Log {
158  listeners = {};
159
160  addEventListener(type, listener) {
161    if (!this.listeners[type]) {
162      this.listeners[type] = [];
163    }
164    if (this.listeners[type].indexOf(listener) === -1) {
165      this.listeners[type].push(listener);
166    }
167  }
168
169  removeEventListener(type) {
170    this.listeners[type] = [];
171  }
172
173  dispatchEvent(type, data) {
174    const listenerArray = this.listeners[type] || [];
175    if (listenerArray.length === 0) return;
176    listenerArray.forEach(listener => {
177      listener.call(this, data);
178    });
179  }
180}

总结

这篇文章封装了weboskect,完美支持了断网重连、自动心跳的功能,且完全兼容原生写法,无任何学习负担,开开箱即用!但美中不足的是,断网重连时间、心跳数据内容目前都是写死的,大家可以根据自己的情况做一些更改,让它更灵活!

参考资料

[1]

https://juejin.cn/post/7358518759118700607: https://juejin.cn/post/7358518759118700607

推荐阅读  点击标题可跳转

1、Node.js 开发者需要知道的 13 个常用库

2、前端需要知道的缓存知识总结

3、TypeScript 初学者快速入门指南 2024 版

个人笔记记录 2021 ~ 2025