封装WebSocket

功能

一般来说,WebSocket应该具有以下功能:

  • 断线重连
  • 单例模式
  • 发布订阅
  • 超时提示
  • 心跳保活
  • 错误处理

源码

源码使用ts编写:

/**
 * @file websocket链接相关
 */

interface SubscribeList {
    [index: string]: Function[]
}

export class Socket {
    private static instance: Socket|null;
    private static _isConnected: boolean = false;

    private readonly cachedUrl: string = '';
    private readonly cachedOnConnected: Function | undefined;

    private socket!: WebSocket;

    // all,通用回调函数
    private cbs: Function[] = [];
    private subscribeList: SubscribeList = {};
    // error,socket连接出错时的回调函数
    private errCbs: Function[] = [];

    // 重试次数
    private reconnectTimes: number = 0;
    private maxReconnectTimes: number = 10;

    private timeout: number = 60 * 1000; // 60s超时
    private timeoutTimer: any = null;

    // 心跳检测由server端触发:适用于服务端释放资源
    // websocket支持心跳检测帧消息 ping/pong opcode 9/10
    // 对于Javascript并没有暴露心跳相关API:https://stackoverflow.com/questions/10585355/sending-websocket-ping-pong-frame-from-browser
    // 实际测试:server端发送ping帧的时候,浏览器会自动回应pong消息

    constructor(url: string, onConnected?: Function) {
        if (Socket.instance) {
            return Socket.instance;
        }

        this.cachedUrl = url;
        this.cachedOnConnected = onConnected;
        this.socket = this.connect(url, onConnected);
        Socket.instance = this;
    }

    static get isConnected(): boolean {
        return this._isConnected;
    }

    connect(url: string, onConnected?: Function) {
        const ws = new WebSocket(url);
        ws.onopen = (...args) => {
            Socket._isConnected = true;
            onConnected && onConnected(...args);
        };
        this.init(ws);
        return ws;
    }

    init(socket: WebSocket) {
        socket.onmessage = (evt: any) => {
            this.timeoutTimer && clearTimeout(this.timeoutTimer);
            this.reconnectTimes = 0;
            let jsonData: any = {};
            try {
                jsonData = JSON.parse(evt.data);
            }
            catch (e) {
                throw new Error('[socket]: data' + evt.data + 'cannot convert to json.');
            }
            finally {
                this.callCbs(this.cbs, jsonData);
                this.callCbs(this.subscribeList[jsonData.Name] || [], jsonData);
            }
        };

        socket.onerror = (...err) => {
            Socket._isConnected = false;
            Socket.instance = null;
            this.reconnected();
            this.errCbs.forEach((cb: Function) => cb(...err));
        };

        socket.onclose = (...reason) => {
            Socket._isConnected = false;
            Socket.instance = null;
        };
    }

    private callCbs(cbs: Function[], jsonData: object) {
        let delIdxList: number[] = [];

        cbs.forEach((cb, idx) => {
            cb(jsonData);
            // @ts-ignore
            if (cb.isOnce) {
                delIdxList.push(idx);
            }
        });

        delIdxList.forEach(it => {
            cbs.splice(it, 1);
        });
    }

    private startTimeOutTimer() {
        this.timeoutTimer = setTimeout(() => {
            this.callCbs(this.errCbs, new Error('[socket]: webSocket接受消息超时!'));
        }, this.timeout);
    }

    sendJSON(data: Object) {
        if (Socket._isConnected) {
            this.socket.send(JSON.stringify(data, null, 4));
            // 设置统一超时处理,不区分某个send请求的60s延迟,发送多个命令之后只要有响应就清除超时定时器
            // 因为只要有响应就说明服务器在正常工作,应继续等待请求处理
            this.startTimeOutTimer();
        }
        else {
            this.callCbs(this.errCbs, new Error('[socket]: 当前WebSocket未连接!'));
        }
    }

    sendPrimitive(data: string) {
        if (Socket._isConnected) {
            this.socket.send(data);
        }
        else {
            this.callCbs(this.errCbs, new Error('[socket]: 当前WebSocket未连接!'));
        }
    }

    private reconnected() {
        if (!Socket._isConnected && this.reconnectTimes < this.maxReconnectTimes) {
            setTimeout(() => {
                this.reconnectTimes++;
                this.socket = this.connect(this.cachedUrl, this.cachedOnConnected);
            }, 2000);
        }
    }

    destroy() {
        this.socket.close(1000);
        Socket.instance = null;
    }

    // 只能remove具名函数
    private removeListener(cbList: Function[], cb?: Function) {
        if (!cb) {
            cbList.length = 0;
            return;
        }

        for (let i = 0; i < cbList.length; i++) {
            const currentCb = cbList[i];
            if (cb === currentCb) {
                cbList.splice(i, 1);
                break;
            }
        }
    }

    unsubscribe(eventName: string, cb?: Function) {
        if (eventName === 'error') {
            this.removeListener(this.errCbs, cb);
        }
        else if (eventName === 'all') {
            this.removeListener(this.cbs, cb);
        }
        else if (this.subscribeList[eventName]) {
            this.removeListener(this.subscribeList[eventName], cb);
        }
    }

    subscribe(eventName: string, cb: Function, once?: boolean) {
        // @ts-ignore
        cb.isOnce = once;
        if (eventName === 'error') {
            this.errCbs.push(cb);
        }
        else if (eventName === 'all') {
            this.cbs.push(cb);
        }
        else {
            if (!this.subscribeList[eventName]) {
                this.subscribeList[eventName] = [cb];
            }
            else {
                this.subscribeList[eventName].push(cb);
            }
        }
    }
}

你可能感兴趣的:(前端杂事)