首页 > 其他分享 >前端WebSocket 封装

前端WebSocket 封装

时间:2023-06-19 16:58:55浏览次数:32  
标签:封装 前端 private wsSocket && WebSocket websocket null options

前言

实际工作中可能会遇到需要封装WebSocket的场景,以下基于ts做了WebSocket的封装,包括心跳机制和重连

1、封装逻辑如下,新建ws文件:

// websocket 封装
enum ConnectionState { // websocket 连接状态
  'CONNECTING',
  'CONNECTED',
  'DISCONNECTED',
  'RECONNECTING',
  'RECOVERY'
}

export interface ConnectCallback {
  (): void;
}

export interface MessageCallback {
  (response: MessageEvent<any>): void;
}

export interface ErrorCallback {
  (error: Error): void;
}

export interface RequestCallback {
  (error: Error): void;
}

export interface SuccessResponseCallback {
  (response: string): void;
}

export interface ErrorResponseCallback {
  (response: string): void;
}

export interface StateChangeCallback {
  (prevState: ConnectionState, state: ConnectionState, retryCount: number): void;
}

export interface ReconnectFailedCallback {
  (): void;
}

export interface HeartCheck {
  timeout: number;
  serverTimeout: number;
  timer: number;
  serverTimer: number;
  reset: () => {};
  start: () => void;
}

export interface Options {
  wsUrl: string;
  extendInfo: Record<string, any>;
  onConnect: ConnectCallback; // websocket连接成功回调
  onMessage: MessageCallback; // 客户接收服务端数据回调
  one rror: ErrorCallback; // 通信发送错误回调
  onWsStateChange?: StateChangeCallback;// websocket连接状态改变的回调
  onWsReconnectFailed?: ReconnectFailedCallback; // 重连失败回调
}

export class WS{
  private wsUrl: string;
  private extendInfo: Record<string, any>;
  private options: Options;
  // websocket
  private wsSocket: WebSocket | null;
  public state: ConnectionState; // websocket当前连接状态
  public prevState: ConnectionState; // websocket上次连接状态
  private currentId: number; // 用于和请求消息匹配
  private times: number; // 超时时间 默认 60s
  private timer: any; // 延迟器
  public autoReconnected: boolean; // 自动重连标志
  private lockReconnect: boolean; // 避免重复连接
  private heartCheck: HeartCheck; // 心跳检测机制
  private retryTimerId: number;// 重连定时器对象
  private retryCount: number; // 当前重连次数
  private maxRetryCount: number; // 最大重连次数
  private pendingRequests: Array<string>; // websocket 未准备好时发送消息,缓存消息的队列
  private successCallbacks: Map<string, SuccessResponseCallback>; // 发送消息,请求成功回调
  private errorCallbacks: Map<string, ErrorResponseCallback>; // 发送消息,请求失败回调

  public constructor (options: Options) {
    console.log('options', options);
    this.wsUrl = options.wsUrl;
    this.options = options;
    this.wsSocket = null;
    this.state = ConnectionState.DISCONNECTED;
    this.prevState = ConnectionState.DISCONNECTED;
    this.times = 60 * 1000;
    this.timer = null;
    this.autoReconnected = true;
    this.lockReconnect = false;
    this.retryTimerId = null;
    this.retryCount = 0;
    this.maxRetryCount = 30;
    this.pendingRequests = new Array<string>();
    this.successCallbacks = new Map<string, SuccessResponseCallback>();
    this.errorCallbacks = new Map<string, ErrorResponseCallback>();
    this.heartCheck = this.initHeartCheck();
  }

  // 建立websocket连接
  public connect (): boolean {
    if (!this.wsUrl && !this.wsUrl.length) {
      console.error('Websocket url is empty!')
      return false
    }
    this.retryTimerId && clearTimeout(this.retryTimerId);
    if (!this.wsSocket) {
      this.updateWsStateChange(ConnectionState.CONNECTING);
      this.wsSocket = new WebSocket(this.wsUrl);
      if (this.wsUrl) {
        this.wsSocket.onopen = this.onConnect.bind(this);
        this.wsSocket.onmessage = this.onWsMessage.bind(this);
        this.wsSocket.onclose = this.onWsClose.bind(this);
        this.wsSocket.onerror = this.onWsError.bind(this);
      }
    }
    return !!this.wsSocket;
  }

  // 发送消息
  public sendMessage (request: any, successCb?: SuccessResponseCallback, errorCb?: ErrorResponseCallback): boolean {
    if (!request) {
      return false;
    }
    const requestId = `${Date.now()}_${this.currentId++}`;
    request.id = requestId; // 需要后端配合,将id带给后端,后端再将id返回
    if (this.wsSocket) {
      if (this.wsSocket.readyState < 1) {
        this.pendingRequests.push(request);
      } else {
        console.log(`send message:${request}`);
        this.send(request)
      }
      if (successCb && typeof successCb === 'function') {
        this.successCallbacks.set(requestId, successCb)
      }
      if (errorCb && typeof errorCb === 'function') {
        this.errorCallbacks.set(requestId, errorCb)
      }
      return true;
    }
  }

  /**
  * 发送消息 + 超时机制
  */
  private send (msg: any) {
    this.wsSocket.send(msg);
    if (!this.timer) {
      this.timer = setTimeout(() => {
        console.error('server timeout')
        if (typeof this.options.onError === 'function') {
          this.options.onError(new Error('server timeout'));
        }
      }, this.times);
    }
  }

  // 关闭websocket连接
  public close (): void {
    if (this.wsSocket) {
      this.autoReconnected = false;
      this.pendingRequests = [];
      this.wsSocket.close();
      this.resetWs();
      this.timer && clearTimeout(this.timer);
      this.retryTimerId && clearTimeout(this.retryTimerId)
      this.heartCheck.reset();
      this.heartCheck = null;
      console.log('close websocket')
    }
  }

  // 清空websocket的绑定事件
  private resetWs () {
    if (this.wsSocket) {
      this.wsSocket.onmessage = null;
      this.wsSocket.onclose = null;
      this.wsSocket.onerror = null;
      this.wsSocket.onopen = null;
      this.wsSocket = null;
    }
  }

  // 连接建立成功时触发
  private onConnect () {
    this.heartCheck.start();
    this.prevState = this.state;
    if (this.retryTimerId) {
      clearTimeout(this.retryTimerId);
      this.retryTimerId = null;
      this.updateWsStateChange(ConnectionState.RECOVERY);
    } else {
      this.updateWsStateChange(ConnectionState.CONNECTED);
    }
    if (this.pendingRequests.length) {
      let req: string | undefined;
      while ((req = this.pendingRequests.shift())) {
        this.send(req)
      }
    }
  }

  // 收到服务端消息时触发
  private onWsMessage (event: MessageEvent<any>) {
    this.heartCheck && this.heartCheck.start();
    this.timer && clearTimeout(this.timer);
    this.timer = null;
    const message = JSON.parse(event.data);
    if (message === 'pong') return
    if (typeof this.options.onMessage === 'function') {
      this.options.onMessage(event)
    }
    // 根据消息id判断是否要执行请求回调
    const id = message.id;
    if (id) {
      const successCb = this.successCallbacks.get(id);
      const errorCb = this.errorCallbacks.get(id);
      // 成功
      successCb(message);
      // 失败
      errorCb(message);
      this.successCallbacks.delete(id);
      this.errorCallbacks.delete(id);
    }
  }

  // 连接关闭时触发
  private onWsClose (event) {
    console.log('onWsClose', event);
    this.heartCheck && this.heartCheck.reset();
    this.pendingRequests = [];
    this.updateWsStateChange(ConnectionState.DISCONNECTED);
  }

  // 连接报错时触发
  private onWsError (event) {
    console.log('onWsError', event);
    this.heartCheck && this.heartCheck.reset();
    this.pendingRequests = [];
    this.updateWsStateChange(ConnectionState.DISCONNECTED);
  }

  /**
  * 更新websocket连接状态
  */
  private updateWsStateChange (state: ConnectionState) {
    this.prevState = this.state;
    this.state = state;
    if (this.options.onWsStateChange && typeof this.options.onWsStateChange === 'function') {
      this.prevState !== this.state && this.options.onWsStateChange(this.prevState, this.state, this.retryCount);
    }
  }

  /**
   * websocket 重连
   */
  public reconnect () {
    if (!this.autoReconnected && this.lockReconnect) return
    this.lockReconnect = true;
    this.retryTimerId && clearTimeout(this.retryTimerId)
    if (this.retryCount < this.maxRetryCount) {
      this.retryTimerId = window.setTimeout(() => {
        this.retryCount++;
        console.log(`${new Date().toLocaleString()} Try to reconnect, count: ${this.retryCount}`);
        this.updateWsStateChange(ConnectionState.RECONNECTING);
        this.resetWs();
        this.connect();
        this.lockReconnect = false;
      }, this.getReconnectDelay(this.retryCount));
    } else {
      console.warn(`SDK has tried reconnect signal channel for ${this.maxRetryCount} times, but all failed. please check your network`);
      if (this.options.onWsReconnectFailed) {
        this.options.onWsReconnectFailed()
      }
    }
  }

  /**
   * 心跳机制
   */
  private initHeartCheck (): HeartCheck {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const that = this;
    return {
      timeout: 2 * 1000, // 每2s向服务端发送一次消息
      serverTimeout: 10 * 1000, // 10s收不到服务端消息算超时
      timer: null,
      serverTimer: null,
      reset () { // 心跳检测重置
        clearTimeout(this.timer);
        clearTimeout(this.serverTimer);
        this.timer = null;
        this.serverTimer = null;
        return this;
      },
      start () { // 心跳检测启动
        this.reset();
        this.timer = setTimeout(() => {
          that.wsSocket.send('ping'); // 定时向服务端发送消息
          if (!this.serverTimer) {
            this.serverTimer = setTimeout(() => {
              console.log(new Date().toLocaleString(), 'not received pong, close the websocket');
              that.reconnect(); // 重连
            }, this.serverTimeout);
          }
        }, this.timeout);
      }
    }
  }

  /**
    * 获取重连的定时时间
    * @param count 重连次数
    */
  private getReconnectDelay (count: number) {
    const t = Math.round(count / 2) + 1;
    return t > 6 ? 13 * 1000 : 3 * 1000;
  }
}

 2、调用

import {WS} from './ws.ts'

const ws = new WS({
    wsUrl:'xxxxx',
    onConnect:()=>{
      // websocket的连接成功业务逻辑
    },
   onMessage:(data)=>{
     // 接收到客户端数据逻辑
   },
   one rror:(err)=>{
    // 通信发送错误逻辑
   }
}) 

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

// 发送消息
ws.sendMessage(msg,(data)=>{
  //成功回调
},(err)=>{
  // 失败回调
})

标签:封装,前端,private,wsSocket,&&,WebSocket,websocket,null,options
From: https://www.cnblogs.com/hyt09/p/17491531.html

相关文章

  • nginx前端页面通过docker部署过程中的相关问题
    1、nginx.conf的ip地址对应服务器的ip 2、数据卷的路径需要与配置文件对应nginx.conf创建数据卷与容器的语句dockerrun--name=nginx01-vhtml:/usr/share/nginx/html-p8080:80-dnginx3、nginx.conf配置文件的存放位置dockercpnginx.confngin......
  • 前端如何防止用户使用F12看控制台
    先分享一下自己的搭的免费的chatGPT网站https://www.hangyejingling.cn/正文1、如果是VUE框架开发,在生产环境中。在入口文件APP.vue中添加如下代码,其他框架同理if(process.env.mode==='production'){ (functionnoDebugger(){ functiontestDebugger(){ vard=......
  • Android dataBinding简单的封装
    一、简介本文是databinding使用的简单封装,主要是在基类BaseActivity和BaseFragment中二、具体步骤1.在build.gradle中开启databindingdataBinding{enabled=true}2.在BaseActivity的封装,主要是通过反射的方式获取。如下packagecom.zw.databindingdemo.java;importandroid.o......
  • 小鹿线Web前端好不好?
    现在web前端开发开发技术在不断地迭代更新,有很多从事前端开发的程序员在技术上会遇到瓶颈,这个时候小伙伴就应该通过不断的学习开发技术知识,来提升自身的开发技术水平,那小伙伴应该怎么来学习呢?1.梳理清楚知识体系框架学习前端开发技术,不管是入门还是进阶,一定都要有知识体系建设......
  • uniapp封装接口
    1.创建一个config文件夹,在里面创建app.jslethttpApi=''//接口公共部分module.exports={//要传的请求头token等HTTP_REQUEST_URL:httpApi,HEADER:{Headers,Authorization:'token','i-branch':'zh'},//......
  • 前端生成付款的二维码
    方形二维码:QRCodeVUE生成二维码(qrcodejs)QRCode.js是用于制作QRCode的javascript库。QRCode.js支持跨浏览器与HTML5Canvas和DOM中的表格标签。QRCode.js没有依赖项。安装依赖npmiqrcodejs2--save完整使用<template><!--方形二维码--><divc......
  • 使用Kotlin+Rretrofit+rxjava+设计模式+MVP封装网络请求
    0、前言:kotlin使用起来非常顺畅,尤其是结合rxjava、设计模式使用,你会发现写代码原来可以这么开心!什么?你还在使用java?赶紧去学一下kotlin吧!我相信你一定会对他爱不释手,kotlin也很容易学,花一天的时间就可以从java切换为kotlin一、正文本文主要介绍如何使用kotlin封装网络请求的工具,结......
  • Tab切换以及倒计时组件封装
    1、Tab组件功能支持默认选中tab子元素可以是文本或者图片自定义tab的数量,并自适应展示实现方式用ul>li标签遍历传入的tabs数组参数渲染判断是否传入背景,未传则显示文字绑定点击事件特点简单易用可适配性2、倒计时组件功能常用于榜单或者活动结束倒计时......
  • vue中前端实现pdf预览(含vue-pdf插件用法)
    场景:前端需要根据后端返回的线上pdf的地址,实现pdf的预览功能。情况一:后端返回的pdf地址,粘贴到浏览器的url框中,是可以在浏览器中直接进行预览的。方法(1)可以直接使用window.open('获取到的pdf地址')重新打开一个浏览器页签,通过浏览器页签直接实现预览功能(预览页面的样式,根据浏览器的不......
  • 前端Vue图片上传组件支持单个文件多个文件上传 自定义上传数量 预览删除图片 图片压缩
    前端Vue图片上传组件支持单个文件多个文件上传自定义上传数量预览删除图片图片压缩,下载完整代码请访问uni-app插件市场址:https://ext.dcloud.net.cn/plugin?id=13099效果图如下:1.0.0(2023-06-18)组件初始化使用方法<!--count:最大上传数量 imageList:图片上传选......