糖糖的tangyuxian-js-socket
某天项目要用到websocket,糖糖就在想又要自己去处理断线重新和发心跳,有没有现成的,轻量化的类库呢,找了很多都没有合适的,好波,那就自己亲自动手写一个吧
npm地址:tangyuxian-js-socket
github仓库地址:tangyuxian-js-socket
注意升级成ts后会有小蓝标哦
一 创建并拉取仓库
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