tangyuxian
文章80
标签37
分类5

文章分类

文章归档

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

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

糖糖的tangyuxian-js-socket

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

npm地址:tangyuxian-js-socket

github仓库地址:tangyuxian-js-socket

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

image-20210925175939243

img

一 创建并拉取仓库

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

二 初始化NPM

1
npm init

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//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进行了重写

原版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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也非常简单,注意增加了强类型约束,方便类型推导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
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配置两种打包方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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对象并挂载到全局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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)
  })
})

七 发布

1
2
3
4
5
6
7
8
9
# 登录 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 协议进行许可