首页/文章/javascript

websocket封装

2024-06-14
23538 分钟
...

简介

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

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

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

websokect的API非常简单

// 创建ws连接
const ws = new WebSocket('ws://localhost:8080/test');
ws.onopen = function() {  
  console.log('WebSocket 连接已经建立。');
  ws.send('Hello, server!');
};
ws.onmessage = function(event) {  
  console.log('收到服务器消息:',event.data);
};
ws.onerror = function(event) {  
  console.error('WebSocket 连接出现错误:'event);
};
ws.onclose = function() {   
  console.log('WebSocket 连接已经关闭。');
}

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

封装成功演示

核心优势

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

import WebSocketClient from "./WebSocketClient"
// 创建实例
const ws = new WebSocketClient('ws://localhost:3200');
// 连接
ws.connect()
// 同原生方法
ws.onclose(()=>{})
// 同原生方法
ws.onerror(()=>{})
// 同原生方法
ws.onmessage(()=>{  
  // 同原生方法  ws.send("自定义发送的数据")
})
// 同原生方法
ws.onopen(()=>{})
// 关闭连接
ws.close()

效果演示

后端服务创建

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

npm install ws

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

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

我们启动这个服务

node index.js

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

前端websokect测试

我们先写前端的相关逻辑

import { WebSocketClient } from '@/utils/dataDispatcher/WebSocketClient';
const ws = new WebSocketClient('ws://localhost:3200');
// 连接
ws.connect();
// 同原生方法
ws.onclose(() => {});
// 同原生方法
ws.onerror(() => {});
// 同原生方法
ws.onmessage(() => {  
  // 同原生方法   
  ws.send('自定义发送的数据');
});
// 同原生方法
ws.onopen(() => {});

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

心跳验证:

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

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

断网重连验证:

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

技术路线

基本框架搭建

export class WebSocketClient {   
  // #socket链接   
  private url = '';   
// #socket实例    
private socket: WebSocket | null = null;   
constructor(url: string) {   
  super();    
  this.url = url;   
}      
// >消息发送 
public send(message: string): void {     
  if (this.socket && this.socket.readyState === WebSocket.OPEN) {     
    this.socket.send(message);   
else {          
  console.error('[WebSocket] 未连接'); 
}    
}    
// !初始化连接 
public connect(): void {     
  if (this.socket && this.socket.readyState === Web
       Socket.OPEN) {     
    return;     
  }     
  this.socket = new WebSocket(this.url);  
// !websocket连接成功      
this.socket.onopen = event => {   
  console.log(`连接成功,等待服务端数据推送[onopen]...`);  
};      
this.socket.onmessage = event => { 
};       
this.socket.onclose = event => {  
  console.log(`连接断开[onclose]...`); 
};      
this.socket.onerror = event => {      
  console.log(`连接异常[onerror]...`);   
};   
}   
// >关闭连接  
public close(): void {     
  if (this.socket) {       
    this.socket.close();   
    this.socket = null;   
  }   
}}

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

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

  • 发送消息到服务器。

  • 关闭连接。

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

断网重连封装

export class WebSocketClient{
  // #socket链接
  private url = '';
// #socket实例
private socket: WebSocket | null = null;
// #重连次数
private reconnectAttempts = 0;
// #最大重连数
private maxReconnectAttempts = 5;
// #重连间隔
private reconnectInterval = 10000; // 10 seconds

constructor(url: string) {
  super();
  this.url = url;
}
// >消息发送
public send(message: string): void {
  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
    this.socket.send(message);
  } else {
  console.error('[WebSocket] 未连接');
}
}

// !初始化连接
public connect(): void {
  if (this.reconnectAttempts === 0) {
    console.log(`初始化连接中...`);
  }
  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
    return;
  }
this.socket = new WebSocket(this.url);

// !websocket连接成功
this.socket.onopen = event => {
  // 重置重连尝试成功连接
  this.reconnectAttempts = 0;
  console.log(`连接成功,等待服务端数据推送[onopen]...`);
};

this.socket.onmessage = event => {
};

this.socket.onclose = event => {
  if (this.reconnectAttempts === 0) {
    console.log(`连接断开[onclose]...`);
  }
  if (!this.stopWs) {
    this.handleReconnect();
  }
};

this.socket.onerror = event => {
  if (this.reconnectAttempts === 0) {
    console.log(`连接异常[onerror]...`);
  }
};
}

// > 断网重连逻辑
private handleReconnect(): void {
  if (this.reconnectAttempts < this.maxReconnectAttempts) {
    this.reconnectAttempts++;
    console.log(`尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
    setTimeout(() => {
      this.connect();
    }, this.reconnectInterval);
  } else {
  console.log(`最大重连失败,终止重连: ${this.url}`);
        }
    }

    // >关闭连接
    public close(): void {
        if (this.socket) {
            this.socket.close();
            this.socket = null;
        }
    }
}

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

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

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

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

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

初始化连接并处理事件

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

public connect(): void {
  if (this.reconnectAttempts === 0) {
    console.log(`初始化连接中...`);
  }
  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
    return;
  }
this.socket = new WebSocket(this.url);

this.socket.onopen = (event: Event) => {
  this.reconnectAttempts = 0;
  console.log(`连接成功,等待服务端数据推送[onopen]...`);
};
this.socket.onclose = (event: CloseEvent) => {
  if (this.reconnectAttempts === 0) {
    console.log(`连接断开[onclose]...`);
  }
  this.handleReconnect();
};

this.socket.onerror = (event: Event) => {
  if (this.reconnectAttempts === 0) {
    console.log(`连接异常[onerror]...`);
  }
  this.handleReconnect();
};
}

处理重连逻辑

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

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

关闭连接

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

public close(): void {
  if (this.socket) {
    this.socket.close();
    this.socket = null;
  }
}

自动心跳封装

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

实现自动心跳的基本思路

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

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

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


export class WebSocketClient {
  // #socket链接
  private url = '';
// #socket实例
private socket: WebSocket | null = null;
// #重连次数
private reconnectAttempts = 0;
// #最大重连数
private maxReconnectAttempts = 5;
// #重连间隔
private reconnectInterval = 10000; // 10 seconds
// #发送心跳数据间隔
private heartbeatInterval = 1000 * 30;
// #计时器id
private heartbeatTimer?: NodeJS.Timeout;
// #彻底终止ws
private stopWs = false;
// *构造函数
constructor(url: string) {
  super();
  this.url = url;
}
// >消息发送
public send(message: string): void {
  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
    this.socket.send(message);
  } else {
  console.error('[WebSocket] 未连接');
}
}

// !初始化连接
public connect(): void {
  if (this.reconnectAttempts === 0) {
    console.log('WebSocket', `初始化连接中...`);
  }
  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
    return;
  }
this.socket = new WebSocket(this.url);

// !websocket连接成功
this.socket.onopen = event => {
  this.stopWs = false;
  // 重置重连尝试成功连接
  this.reconnectAttempts = 0;
  // 在连接成功时停止当前的心跳检测并重新启动
  this.startHeartbeat();
  console.log(`连接成功,等待服务端数据推送[onopen]...`);
};

this.socket.onmessage = event => {
  this.dispatchEvent('message', event);
  this.startHeartbeat();
};

this.socket.onclose = event => {
  if (this.reconnectAttempts === 0) {
    console.log(`连接断开[onclose]...`);
  }
  if (!this.stopWs) {
    this.handleReconnect();
  }
};

this.socket.onerror = event => {
  if (this.reconnectAttempts === 0) {
    console.log(`连接异常[onerror]...`);
  }
  this.closeHeartbeat();
};
}

// > 断网重连逻辑
private handleReconnect(): void {
  if (this.reconnectAttempts < this.maxReconnectAttempts) {
    this.reconnectAttempts++;
    console.log('WebSocket', `尝试重连...`);
    setTimeout(() => {
      this.connect();
    }, this.reconnectInterval);
  } else {
  this.closeHeartbeat();
console.log(`最大重连失败,终止重连: ${this.url}`);
}
}

// >关闭连接
public close(): void {
  if (this.socket) {
    this.stopWs = true;
    this.socket.close();
    this.socket = null;
  }
  this.closeHeartbeat();
}

// >开始心跳检测 -> 定时发送心跳消息
private startHeartbeat(): void {
  if (this.stopWs) return;
  if (this.heartbeatTimer) {
    this.closeHeartbeat();
  }
this.heartbeatTimer = setInterval(() => {
  if (this.socket) {
    this.socket.send(JSON.stringify({ type: 'heartBeat', data: {} }));
    console.log('WebSocket', '送心跳数据...');
  } else {
    console.error('[WebSocket] 未连接');
  }
}, this.heartbeatInterval);
}

// >关闭心跳
private closeHeartbeat(): void {
  clearInterval(this.heartbeatTimer);
  this.heartbeatTimer = undefined;
}
}

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

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

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

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

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

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

private startHeartbeat(): void {
  if (this.stopWs) return;
  if (this.heartbeatTimer) {
    this.closeHeartbeat();
  }
this.heartbeatTimer = setInterval(() => {
  if (this.socket) {
    this.socket.send(JSON.stringify({ type: 'heartBeat', data: {} }));
    console.log('WebSocket', '发送心跳数据...');
  } else {
    console.error('[WebSocket] 未连接');
  }
}, this.heartbeatInterval);
}
  • 停止心跳检测

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

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

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

this.socket.onclose = event => {
  if (this.reconnectAttempts === 0) {
    console.log(`连接断开[onclose]...`);
  }
  if (!this.stopWs) {
    this.handleReconnect();
  }
};

this.socket.onerror = event => {
  if (this.reconnectAttempts === 0) {
    console.log(`连接异常[onerror]...`);
  }
  this.closeHeartbeat();
};

如何触发原生函数

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

import { EventDispatcher } from './dispatcher';

export class WebSocketClient extends EventDispatcher {

  //...
  constructor(url: string) {
    super();
    this.url = url;
  }
  // >生命周期钩子
  onopen(callBack: Function) {
    this.addEventListener('open', callBack);
  }
  onmessage(callBack: Function) {
    this.addEventListener('message', callBack);
  }
  onclose(callBack: Function) {
    this.addEventListener('close', callBack);
  }
  onerror(callBack: Function) {
    this.addEventListener('error', callBack);
  }

  // !初始化连接
  public connect(): void {
    // ...

    // !websocket连接成功
    this.socket.onopen = event => {
      // ...
      this.dispatchEvent('open', event);
    };

    this.socket.onmessage = event => {
      this.dispatchEvent('message', event);
      this.startHeartbeat();
    };

    this.socket.onclose = event => {
      // ...
      this.dispatchEvent('close', event);
    };

    this.socket.onerror = event => {
      // ...
      this.closeHeartbeat();
      this.dispatchEvent('error', event);
    };
  }

// >关闭连接
public close(): void {
  if (this.socket) {
    this.stopWs = true;
    this.socket.close();
    this.socket = null;
    this.removeEventListener('open');
    this.removeEventListener('message');
    this.removeEventListener('close');
    this.removeEventListener('error');
  }
  this.closeHeartbeat();
}

// ...
}

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

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

EventDispatcher源码如下:

export class EventDispatcher {
  private listeners: { [type: string]: Function[] } = {};

protected addEventListener(type: string, listener: Function) {
  if (!this.listeners[type]) {
    this.listeners[type] = [];
  }
  if (this.listeners[type].indexOf(listener) === -1) {
    this.listeners[type].push(listener);
  }
}

protected removeEventListener(type: string) {
  this.listeners[type] = [];
}

protected dispatchEvent(type: string, data: any) {
  const listenerArray = this.listeners[type] || [];
  if (listenerArray.length === 0) return;
  listenerArray.forEach(listener => {
    listener.call(this, data);
  });
}
}

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

juejin.cn/post/735851…[1]

完整代码

ts版本

import { EventDispatcher } from './dispatcher';

export class WebSocketClient extends EventDispatcher {
  // #socket链接
  private url = '';
  // #socket实例
  private socket: WebSocket | null = null;
  // #重连次数
  private reconnectAttempts = 0;
  // #最大重连数
  private maxReconnectAttempts = 5;
  // #重连间隔
  private reconnectInterval = 10000; // 10 seconds
  // #发送心跳数据间隔
  private heartbeatInterval = 1000 * 30;
  // #计时器id
  private heartbeatTimer?: NodeJS.Timeout;
  // #彻底终止ws
  private stopWs = false;
  // *构造函数
  constructor(url: string) {
    super();
    this.url = url;
  }
  // >生命周期钩子
  onopen(callBack: Function) {
    this.addEventListener('open', callBack);
  }
  onmessage(callBack: Function) {
    this.addEventListener('message', callBack);
  }
  onclose(callBack: Function) {
    this.addEventListener('close', callBack);
  }
  onerror(callBack: Function) {
    this.addEventListener('error', callBack);
  }
  // >消息发送
  public send(message: string): void {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(message);
    } else {
      console.error('[WebSocket] 未连接');
    }
  }

  // !初始化连接
  public connect(): void {
    if (this.reconnectAttempts === 0) {
      this.log('WebSocket', `初始化连接中...          ${this.url}`);
    }
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      return;
    }
    this.socket = new WebSocket(this.url);

    // !websocket连接成功
    this.socket.onopen = event => {
      this.stopWs = false;
      // 重置重连尝试成功连接
      this.reconnectAttempts = 0;
      // 在连接成功时停止当前的心跳检测并重新启动
      this.startHeartbeat();
      this.log('WebSocket', `连接成功,等待服务端数据推送[onopen]...     ${this.url}`);
      this.dispatchEvent('open', event);
    };

    this.socket.onmessage = event => {
      this.dispatchEvent('message', event);
      this.startHeartbeat();
    };

    this.socket.onclose = event => {
      if (this.reconnectAttempts === 0) {
        this.log('WebSocket', `连接断开[onclose]...    ${this.url}`);
      }
      if (!this.stopWs) {
        this.handleReconnect();
      }
      this.dispatchEvent('close', event);
    };

    this.socket.onerror = event => {
      if (this.reconnectAttempts === 0) {
        this.log('WebSocket', `连接异常[onerror]...    ${this.url}`);
      }
      this.closeHeartbeat();
      this.dispatchEvent('error', event);
    };
  }

  // > 断网重连逻辑
  private handleReconnect(): void {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      this.log('WebSocket', `尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})       ${this.url}`);
      setTimeout(() => {
        this.connect();
      }, this.reconnectInterval);
    } else {
      this.closeHeartbeat();
      this.log('WebSocket', `最大重连失败,终止重连: ${this.url}`);
    }
  }

  // >关闭连接
  public close(): void {
    if (this.socket) {
      this.stopWs = true;
      this.socket.close();
      this.socket = null;
      this.removeEventListener('open');
      this.removeEventListener('message');
      this.removeEventListener('close');
      this.removeEventListener('error');
    }
    this.closeHeartbeat();
  }

  // >开始心跳检测 -> 定时发送心跳消息
  private startHeartbeat(): void {
    if (this.stopWs) return;
    if (this.heartbeatTimer) {
      this.closeHeartbeat();
    }
    this.heartbeatTimer = setInterval(() => {
      if (this.socket) {
        this.socket.send(JSON.stringify({ type: 'heartBeat', data: {} }));
        this.log('WebSocket', '送心跳数据...');
      } else {
        console.error('[WebSocket] 未连接');
      }
    }, this.heartbeatInterval);
  }

  // >关闭心跳
  private closeHeartbeat(): void {
    clearInterval(this.heartbeatTimer);
    this.heartbeatTimer = undefined;
  }
}
class Log {
  private static console = true;
  log(title: string, text: string) {
    if (!Log.console) return;
    if (import.meta.env.MODE === 'production') return;
    const color = '#ff4d4f';
    console.log(
      `%c ${title} %c ${text} %c`,
      `background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
      `border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
      'background:transparent'
    );
  }
  closeConsole() {
    Log.console = false;
  }
}
export class EventDispatcher extends Log {
  private listeners: { [type: string]: Function[] } = {};

  protected addEventListener(type: string, listener: Function) {
    if (!this.listeners[type]) {
      this.listeners[type] = [];
    }
    if (this.listeners[type].indexOf(listener) === -1) {
      this.listeners[type].push(listener);
    }
  }

  protected removeEventListener(type: string) {
    this.listeners[type] = [];
  }

  protected dispatchEvent(type: string, data: any) {
    const listenerArray = this.listeners[type] || [];
    if (listenerArray.length === 0) return;
    listenerArray.forEach(listener => {
      listener.call(this, data);
    });
  }
}

js版本

export class WebSocketClient extends EventDispatcher {
  // #socket链接
  url = '';
  // #socket实例
  socket = null;
  // #重连次数
  reconnectAttempts = 0;
  // #最大重连数
  maxReconnectAttempts = 5;
  // #重连间隔
  reconnectInterval = 10000; // 10 seconds
  // #发送心跳数据间隔
  heartbeatInterval = 1000 * 30;
  // #计时器id
  heartbeatTimer = undefined;
  // #彻底终止ws
  stopWs = false;
  // *构造函数
  constructor(url) {
    super();
    this.url = url;
  }
  // >生命周期钩子
  onopen(callBack) {
    this.addEventListener('open', callBack);
  }
  onmessage(callBack) {
    this.addEventListener('message', callBack);
  }
  onclose(callBack) {
    this.addEventListener('close', callBack);
  }
  onerror(callBack) {
    this.addEventListener('error', callBack);
  }
  // >消息发送
  send(message) {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(message);
    } else {
      console.error('[WebSocket] 未连接');
    }
  }

  // !初始化连接
  connect() {
    if (this.reconnectAttempts === 0) {
      this.log('WebSocket', `初始化连接中...          ${this.url}`);
    }
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      return;
    }
    this.socket = new WebSocket(this.url);

    // !websocket连接成功
    this.socket.onopen = event => {
      this.stopWs = false;
      // 重置重连尝试成功连接
      this.reconnectAttempts = 0;
      // 在连接成功时停止当前的心跳检测并重新启动
      this.startHeartbeat();
      this.log('WebSocket', `连接成功,等待服务端数据推送[onopen]...     ${this.url}`);
      this.dispatchEvent('open', event);
    };

    this.socket.onmessage = event => {
      this.dispatchEvent('message', event);
      this.startHeartbeat();
    };

    this.socket.onclose = event => {
      if (this.reconnectAttempts === 0) {
        this.log('WebSocket', `连接断开[onclose]...    ${this.url}`);
      }
      if (!this.stopWs) {
        this.handleReconnect();
      }
      this.dispatchEvent('close', event);
    };

    this.socket.onerror = event => {
      if (this.reconnectAttempts === 0) {
        this.log('WebSocket', `连接异常[onerror]...    ${this.url}`);
      }
      this.closeHeartbeat();
      this.dispatchEvent('error', event);
    };
  }

  // > 断网重连逻辑
  handleReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      this.log('WebSocket', `尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})       ${this.url}`);
      setTimeout(() => {
        this.connect();
      }, this.reconnectInterval);
    } else {
      this.closeHeartbeat();
      this.log('WebSocket', `最大重连失败,终止重连: ${this.url}`);
    }
  }

  // >关闭连接
  close() {
    if (this.socket) {
      this.stopWs = true;
      this.socket.close();
      this.socket = null;
      this.removeEventListener('open');
      this.removeEventListener('message');
      this.removeEventListener('close');
      this.removeEventListener('error');
    }
    this.closeHeartbeat();
  }

  // >开始心跳检测 -> 定时发送心跳消息
  startHeartbeat() {
    if (this.stopWs) return;
    if (this.heartbeatTimer) {
      this.closeHeartbeat();
    }
    this.heartbeatTimer = setInterval(() => {
      if (this.socket) {
        this.socket.send(JSON.stringify({ type: 'heartBeat', data: {} }));
        this.log('WebSocket', '送心跳数据...');
      } else {
        console.error('[WebSocket] 未连接');
      }
    }, this.heartbeatInterval);
  }

  // >关闭心跳
  closeHeartbeat() {
    clearInterval(this.heartbeatTimer);
    this.heartbeatTimer = undefined;
  }
}
class Log {
  static console = true;
  log(title, text) {
    if (!Log.console) return;
    if (import.meta.env.MODE === 'production') return;
    const color = '#ff4d4f';
    console.log(
      `%c ${title} %c ${text} %c`,
      `background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
      `border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
      'background:transparent'
    );
  }
  closeConsole() {
    Log.console = false;
  }
}
export class EventDispatcher extends Log {
  listeners = {};

  addEventListener(type, listener) {
    if (!this.listeners[type]) {
      this.listeners[type] = [];
    }
    if (this.listeners[type].indexOf(listener) === -1) {
      this.listeners[type].push(listener);
    }
  }

  removeEventListener(type) {
    this.listeners[type] = [];
  }

  dispatchEvent(type, data) {
    const listenerArray = this.listeners[type] || [];
    if (listenerArray.length === 0) return;
    listenerArray.forEach(listener => {
      listener.call(this, data);
    });
  }
}

总结

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

参考资料

[1]

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

推荐阅读  点击标题可跳转

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

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

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

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2026-02-24
navigator.sendBeacon全指南
在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户 关闭页面 、 刷新 或 跳转路由 时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。 但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— 。 一、 痛点与传统方案的挣扎 场景还原 当用户点击关闭按钮时,浏览器会触发生命周期事件( 或 )。如果我们直接使用普通的异步 AJAX ( 或 ) 发送请求,浏览...
学习
javascript2025-11-02
理解浏览器事件系统,从用户点击到事件对象的完整旅程
深入理解浏览器事件系统:从用户点击到事件对象的完整旅程 “当我点击页面按钮时,背后发生了什么?为什么回调函数能收到一个包含丰富信息的event对象?今天,让我们一起揭开浏览器事件系统的神秘面纱。” 一个令人困惑的现象 作为前端开发者,我们每天都在写这样的代码: 这段代码如此熟悉,以至于我们很少停下来思考:​ ​这个 对象到底从哪里来?它为什么能知道点击的精确坐标?为什么能识别是哪个元素被点击了?​ ​ 更神奇的是,当我们手动创建事件时:...
学习
javascript2025-10-01
实现大文件上传全流程详解
在日常开发中,大文件上传是个绕不开的坎——动辄几百 MB 甚至 GB 级的文件,直接上传不仅容易超时,还会让用户体验大打折扣。最近我用 Vue+Express 实现了一套完整的大文件上传方案,支持分片上传、断点续传、秒传和手动中。 一、先看效果:我们要实现什么? 先上核心功能清单,确保大家明确目标,知道我们要解决哪些实际问题: 大文件分片上传 :将文件切成固定大小的小片段分批上传,避免单次请求超时 秒传 :服务器已存在完整文件时,直接返...
学习
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-15
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一遍 1\. 最偷懒:原生 一行代码就能跑,浏览器帮你搞定。 🔍 关键决策点 2020 年后现代浏览器全覆盖,IE 全军覆没。 必须写死 ,否则 CLS 会抖成 PPT。 适用场景...
学习
javascript2025-09-10
🚀 Web Worker让你的应用丝滑
🌟 引言 在日常的前端开发中,你是否遇到过这样的困扰: 大数据处理时页面卡死 :处理几万条数据时,页面直接卡成PPT,用户点击毫无反应 复杂计算阻塞UI :图片处理、数据分析等计算密集型任务让整个应用假死 文件上传/下载卡顿 :大文件操作时,其他功能完全无法使用 实时数据处理性能差 :WebSocket接收大量数据时,页面渲染严重滞后 今天分享6个Web Worker的核心技巧,让你的应用告别卡顿,用户体验丝滑如德芙! 💡 核心技巧...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录