tangyuxian
文章54
标签26
分类4
ts-记录自己用ts封装一个最萌最轻量的类库

ts-记录自己用ts封装一个最萌最轻量的类库

糖糖的tangyuxian-js-socket

某天项目要用到websocket,糖糖就在想又要自己去处理断线重新和发心跳,有没有现成的,轻量化的类库呢,找了很多都没有合适的,好波,那就自己亲自动手写一个吧

npm地址:tangyuxian-js-socket

github仓库地址:tangyuxian-js-socket

注意升级成ts后会有小蓝标

image-20210925175939243

img

一 创建并拉取仓库

git clone https://github.com/tangyuxian/tangyuxian-js-socket.git

二 初始化NPM

npm init

三 这样配置项目并安装依赖

//package.json
{
  "name": "tangyuxian-js-socket", //项目名
  "version": "2.0.0", //版本号
  "description": "Encapsulating socket class for TS", //描述
  "keywords": [
    "websocket",
    "es6",
    "socket",
    "typescirpt"
  ], //标签
  "author": {
    "name": "tangyuxian", 
    "email": "tangyuxian@vip.qq.com",
    "url": "http://www.tangyuxian.com"
  }, //作者联系方式
  "repository": {
    "type": "git",
    "url": "git+https://github.com/tangyuxian/tangyuxian-js-socket.git"
  }, //项目地址
  "main": "lib/Socket.js", //主程序入口
  "publishConfig": {
    "access": "public"
  },
  "engines": {
    "node": "> 8"
  },
  "files": [
    "dist",
    "lib",
    "@types"
  ], //涉及文件位置
  "unpkg": "./dist/index.min.js", //upkg路径
  "types": "@types/index.d.ts",
  "license": "MIT", //开源声明
  "bugs": {
    "url": "https://github.com/tangyuxian/tangyuxian-js-socket/issues"
  }, //bug收集地址
  "homepage": "https://github.com/tangyuxian/tangyuxian-js-socket#readme", //介绍文档主地址
  "scripts": {
    "bootstrap": "yarn || npm i",
    "build": "rollup -c rollup.config.js",
    "test": "jest",
    "release": "standard-version"
  },
  "devDependencies": {
    "@babel/plugin-transform-modules-commonjs": "^7.15.4",
    "@babel/preset-env": "^7.15.0",
    "@rollup/plugin-babel": "^5.3.0",
    "@types/jest": "^27.0.0", 
    "@types/jsdom": "^16.2.13", 
    "@typescript-eslint/eslint-plugin": "^4.29.1",
    "@typescript-eslint/parser": "^4.29.1",
    "eslint": "^7.32.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^3.4.0",
    "jest": "^27.0.6",//单元测试工具
    "prettier": "^2.3.2",
    "rollup": "^2.56.2",
    "rollup-plugin-terser": "^7.0.2",
    "rollup-plugin-typescript2": "^0.30.0",
    "standard-version": "^9.3.1",
    "ts-jest": "^27.0.4",
    "tslib": "^2.3.1",
    "typescript": "^4.3.5"
  },
  "dependencies": {
    "jsdom": "^17.0.0"//模拟dom操作库
  }
}

四 编写代码

一开始糖糖是用js写的,后来使用ts进行了重写

原版:

class Socket {
  constructor(socketUrl, option) {
    this.socketUrl = socketUrl
    this.option = {
      onOpenAutoSendMsg:"",
      heartTime: 5000, // 心跳时间间隔
      heartMsg: 'ping', // 心跳信息,默认为'ping'
      isReconnect: true, // 是否自动重连
      reconnectTime: 5000, // 重连时间间隔
      reconnectCount: -1, // 重连次数 -1 则不限制
      openCallback: null, // 连接成功的回调
      closeCallback: null, // 关闭的回调
      messageCallback: null, // 消息的回调
      errorCallback: null, // 错误的回调
      debug: false,  //是否打开debug模式
      ...option,
    }
    this.websocket = null
    this.sendPingInterval = null  //心跳定时器
    this.reconnectInterval = null  //重连定时器
    this.activeLink = true  //socket对象是否可用
    this.reconnectNum = 0 //重连次数限制
    this.init()
  }

  /**
   * 初始化
   */
  init() {
    if (!('WebSocket' in window)) {
      throw new Error('当前浏览器不支持')
    }
    if (!this.socketUrl) {
      throw new Error('请配置连接地址')
    }
    Reflect.deleteProperty(this, this.websocket)
    this.websocket = new WebSocket(this.socketUrl)
    this.websocketOnOpen()
    this.websocketOnMessage()
    this.websocketOnError()
    this.websocketOnClose()
  }

  /**
   * 连接成功
   */
  websocketOnOpen(callback) {
    this.websocket.onopen = (event) => {
      if (this.option.debug) console.log('%c websocket链接成功', 'color:green')
      this.sendPing(this.option.heartTime, this.option.heartMsg);
      if(this.option.onOpenAutoSendMsg){
        this.send(this.option.onOpenAutoSendMsg)
      }
      if (typeof callback === 'function') {
        callback(event)
      } else {
        (typeof this.option.openCallback === 'function') && this.option.openCallback(event)
      }
    }

  }

  /**
   * 发送数据
   * @param message
   */
  send (message){
    if (this.websocket.readyState !== this.websocket.OPEN) {
      new Error('没有连接到服务器,无法发送消息')
      return
    }
    this.websocket.send(message)
  }

  /**
   * 触发接收消息事件
   * @param callback
   */
  websocketOnMessage(callback) {
    this.websocket.onmessage = (event) => {
      // 收到任何消息,重新开始倒计时心跳检测
      if (typeof callback === 'function') {
        callback(event.data)
      } else {
        (typeof this.option.messageCallback === 'function') && this.option.messageCallback(event.data)
      }
    }
  }

  /**
   * 连接错误
   * @param callback
   */
  websocketOnError(callback) {
    this.websocket.onerror = (event) => {
      if (this.option.debug) console.error('连接发生错误', event)
      if (typeof callback === 'function') {
        callback(event)
      } else {
        (typeof this.option.errorCallback === 'function') && this.option.errorCallback(event)
      }
    }
  }

  /**
   * 连接关闭
   */
  websocketOnClose(e) {
    this.websocket.onclose = (event) => {
      if (this.option.debug) console.warn('socket连接关闭,关于原因:', event)
      clearInterval(this.sendPingInterval)
      clearInterval(this.reconnectInterval);
      if (this.activeLink && this.option.isReconnect) {
        this.onReconnect()
      } else {
        this.activeLink = false;
        if (this.option.debug) console.log('%c websocket链接完全关闭', 'color:green')
      }
      if (typeof callback === 'function') {
        callback(event)
      } else {
        (typeof this.option.closeCallback === 'function') && this.option.closeCallback(event)
      }
    }
  }

  /**
   * 连接事件
   */
  onReconnect() {
    if (this.option.debug) console.warn(`非正常关闭,${this.option.reconnectTime}毫秒后触发重连事件`)
    if (this.option.reconnectCount === -1 || this.option.reconnectCount > this.reconnectNum) {
      this.reconnectInterval = setTimeout(() => {
        this.reconnectNum++
        if (this.option.debug) console.warn(`正在准备第${this.reconnectNum}次重连`)
        this.init()
      }, this.option.reconnectTime)
    } else {
      this.activeLink = false;
      if (this.option.debug) console.warn(`已重连${this.reconnectNum}次仍然没有响应,取消重连`)
      clearInterval(this.reconnectInterval);
    }
  }

  /**
   * 移除socket并关闭
   */
  removeSocket() {
    this.activeLink = false
    this.websocket.close(1000)
  }


  /**
   * 心跳机制
   * @param time
   * @param ping
   */
  sendPing (time = 5000, ping = 'ping'){
    clearInterval(this.sendPingInterval);
    if (time === -1) return
    this.send(ping)
    this.sendPingInterval = setInterval(() => {
      this.send(ping)
    }, time)
  }

  /**
   * 返回websocket实例
   * @returns {null}
   */
  getWebsocket() {
    return this.websocket
  }

  /**
   * 查看连接状态
   */
  getActiveLink() {
    return this.activeLink
  }
}

export default Socket

其实改成typescript也非常简单,注意增加了强类型约束,方便类型推导

class Socket {
  private socketUrl: string;
  private option: {
    heartTime: number;
    errorCallback: Function | null;
    openCallback: Function | null;
    debug: boolean;
    reconnectTime: number;
    reconnectCount: number;
    closeCallback: Function | null;
    heartMsg: string;
    onOpenAutoSendMsg: string;
    isReconnect: boolean;
    messageCallback: Function | null;
  };
  private websocket: WebSocket | null;
  private sendPingInterval: any;
  private reconnectInterval: any;
  private activeLink: boolean;
  private reconnectNum: number = 0;

  constructor(socketUrl: string, option: object) {
    this.socketUrl = socketUrl;
    this.option = {
      onOpenAutoSendMsg: "",
      heartTime: 5000, // 心跳时间间隔
      heartMsg: "ping", // 心跳信息,默认为'ping'
      isReconnect: true, // 是否自动重连
      reconnectTime: 5000, // 重连时间间隔
      reconnectCount: -1, // 重连次数 -1 则不限制
      openCallback: null, // 连接成功的回调
      closeCallback: null, // 关闭的回调
      messageCallback: null, // 消息的回调
      errorCallback: null, // 错误的回调
      debug: false,  //是否打开debug模式
      ...option
    };
    this.websocket = null;
    this.sendPingInterval = null;  //心跳定时器
    this.reconnectInterval = null;  //重连定时器
    this.activeLink = true;  //socket对象是否可用
    this.reconnectNum = 0; //重连次数限制
    this.init();
  }

  /**
   * 初始化
   */
  init() {
    if (!("WebSocket" in window)) {
      throw new Error("当前浏览器不支持");
    }
    if (!this.socketUrl) {
      throw new Error("请配置连接地址");
    }
    this.websocket = null;
    this.websocket = new window.WebSocket(this.socketUrl);
    this.websocketOnOpen(null);
    this.websocketOnMessage(null);
    this.websocketOnError(null);
    this.websocketOnClose(null);
  }

  /**
   * 连接成功
   */
  websocketOnOpen(callback: Function | null) {
    if(!(this.websocket instanceof window.WebSocket)) return;
    this.websocket.onopen = (event) => {
      if (this.option.debug) console.log("%c websocket链接成功", "color:green");
      this.sendPing(this.option.heartTime, this.option.heartMsg);
      if (this.option.onOpenAutoSendMsg) {
        this.send(this.option.onOpenAutoSendMsg);
      }
      if (typeof callback === "function") {
        callback(event);
      } else {
        (typeof this.option.openCallback === "function") && this.option.openCallback(event);
      }
    };

  }

  /**
   * 发送数据
   * @param message
   */
  send(message: any) {
    if(!(this.websocket instanceof window.WebSocket)) return;
    if (this.websocket.readyState !== this.websocket.OPEN) {
      new Error("没有连接到服务器,无法发送消息");
      return;
    }
    this.websocket.send(message);
  }

  /**
   * 触发接收消息事件
   * @param callback
   */
  websocketOnMessage(callback: Function | null) {
    if(!(this.websocket instanceof window.WebSocket)) return;
    this.websocket.onmessage = (event) => {
      // 收到任何消息,重新开始倒计时心跳检测
      if (typeof callback === "function") {
        callback(event.data);
      } else {
        (typeof this.option.messageCallback === "function") && this.option.messageCallback(event.data);
      }
    };
  }

  /**
   * 连接错误
   * @param callback
   */
  websocketOnError(callback: Function | null) {
    if(!(this.websocket instanceof window.WebSocket)) return;
    this.websocket.onerror = (event) => {
      if (this.option.debug) console.error("连接发生错误", event);
      if (typeof callback === "function") {
        callback(event);
      } else {
        (typeof this.option.errorCallback === "function") && this.option.errorCallback(event);
      }
    };
  }

  /**
   * 连接关闭
   */
  websocketOnClose(callback: Function | null) {
    if(!(this.websocket instanceof window.WebSocket)) return;
    this.websocket.onclose = (event) => {
      if (this.option.debug) console.warn("socket连接关闭,关于原因:", event);
      clearInterval(this.sendPingInterval);
      clearInterval(this.reconnectInterval);
      if (this.activeLink && this.option.isReconnect) {
        this.onReconnect();
      } else {
        this.activeLink = false;
        if (this.option.debug) console.log("%c websocket链接完全关闭", "color:green");
      }
      if (typeof callback === "function") {
        callback(event);
      } else {
        (typeof this.option.closeCallback === "function") && this.option.closeCallback(event);
      }
    };
  }

  /**
   * 连接事件
   */
  onReconnect() {
    if (this.option.debug) console.warn(`非正常关闭,${this.option.reconnectTime}毫秒后触发重连事件`);
    if (this.option.reconnectCount === -1 || this.option.reconnectCount > this.reconnectNum) {
      this.reconnectInterval = setTimeout(() => {
        this.reconnectNum++;
        if (this.option.debug) console.warn(`正在准备第${this.reconnectNum}次重连`);
        this.init();
      }, this.option.reconnectTime);
    } else {
      this.activeLink = false;
      if (this.option.debug) console.warn(`已重连${this.reconnectNum}次仍然没有响应,取消重连`);
      clearInterval(this.reconnectInterval);
    }
  }

  /**
   * 移除socket并关闭
   */
  removeSocket() {
    this.activeLink = false;
    if(!(this.websocket instanceof window.WebSocket)) return;
    this.websocket.close(1000);
  }


  /**
   * 心跳机制
   * @param time
   * @param ping
   */
  sendPing(time = 5000, ping = "ping") {
    clearInterval(this.sendPingInterval);
    if (time === -1) return;
    this.send(ping);
    this.sendPingInterval = setInterval(() => {
      this.send(ping);
    }, time);
  }

  /**
   * 返回websocket实例
   * @returns {null}
   */
  getWebsocket() {
    return this.websocket;
  }

  /**
   * 查看连接状态
   */
  getActiveLink() {
    return this.activeLink;
  }
}

export default Socket;

五 打包

在rollup.config.js配置两种打包方式

import typescript from 'rollup-plugin-typescript2'
import babel from '@rollup/plugin-babel'
import { terser as uglify } from 'rollup-plugin-terser'
import path from 'path'

/**
 * @type {import('rollup').RollupOptions[]}
 */
const config = [
    //打包成js文件,可通过npm安装使用
  {
    input: path.resolve('./src/main.ts'),
    cache: true,
    output: [
      {
        // file: path.resolve('./lib/main.js'),
        file: path.resolve('./lib/Socket.js'),
        format: 'es'
      }
    ],
    plugins: [
      typescript({
        tsconfig: path.resolve('./tsconfig.json')
      }),
      babel({
        extensions: ['.ts']
      })
    ]
  },
    //通过umd形式打包,可打包成压缩版,通过script标签直接引入使用,并将类注册到全局
  {
    input: path.resolve('./src/main.ts'),
    output: {
      file: path.resolve('./dist/index.min.js'),
      format: 'umd',
      name: 'Socket'
    },
    plugins: [
      typescript({
        tsconfig: path.resolve('./tsconfig.json')
      }),
      babel({
        babelrc: false,
        presets: [
          [
            '@babel/env',
            {
              useBuiltIns: 'usage',
              targets: {
                node: 8,
                browsers: ['ie > 8']
              }
            }
          ]
        ],
        extensions: ['.ts']
      }),
      uglify()
    ]
  }
]

export default config

六 编写测试类

因为糖糖封装的类库是依赖浏览器的window对象,那怎么办呢,可以使用jsdom,即可拿到window对象并挂载到全局

import TsSocket from '../src/main'
import JsSocket from '../lib/Socket.js'
import {JSDOM} from 'jsdom'
const { window } = new JSDOM('<!doctype html><html><body></body></html>'); //导出JSDOM中的window对象
// @ts-ignore
global.window = window; //将window对象设置为nodejs中全局对象;
describe('Socket', () => {
  const url = 'ws://127.0.0.101:8888/websocket'
  const option = {
    debug: true,
    onOpenAutoSendMsg: JSON.stringify({ id: '123456', type: 'login' }),
    openCallback: (res: any) => {
      console.log('建立连接成功', res)
      //...
    },
    messageCallback: (res: any) => {
      console.log('接收到的消息', res)
      //...
    }
    //...
  }
  /**
   * @jest-environment jsdom
   */
  it('typeScript', () => {
    const ws = new TsSocket(url, option)
    //...
    jest.setTimeout(5000)
    // ws.removeSocket()
    let getActiveLink = ws.getActiveLink();
    expect(getActiveLink).toBe(true)
  })

  /**
   * @jest-environment jsdom
   */
  it('javascript', () => {
    const ws = new JsSocket(url, option)
    //...
    jest.setTimeout(5000)
    // ws.removeSocket()
    let getActiveLink = ws.getActiveLink();
    expect(getActiveLink).toBe(true)
  })
})

七 发布

# 登录 npm
npm adduser
Username: youthcity
Password:
Email: (this IS public) 填写邮箱
Logged in as youthcity on https://registry.npmjs.org/.

# 发布包
npm publish

img

本文作者:tangyuxian
本文链接:https://www.tangyuxian.com/2021/09/25/%E5%89%8D%E7%AB%AF/TypeScript/ts-%E8%AE%B0%E5%BD%95%E8%87%AA%E5%B7%B1%E7%94%A8ts%E5%B0%81%E8%A3%85%E4%B8%80%E4%B8%AA%E6%9C%80%E8%90%8C%E6%9C%80%E8%BD%BB%E9%87%8F%E7%9A%84%E7%B1%BB%E5%BA%93/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可