WebSocket 浏览器端封装记录

2,321 阅读5分钟

前言:之前有个项目需要使用 WebSocket 通信,自己开发的时候使用 Node.js socket.io 实现的,但是后来生产环境的服务器系统是 windows,用了 C# 处理 WebSocket。于是决定采用浏览器端自带的 WebSocket 通信, 但是原生的 API 有些少,于是决定再封装一下。为了无(尽)缝(量)从 socket.io 中过渡过来,于是综合考虑制定了如下封装目标:

  • 实现 socket.io: on() emit() .
  • 通过 onmessage(e)e.data.eventType 做各流程处理

实现第一个目标:借助 EventHandle

EventHandle 源码
/**
 * 事件处理类
 */

// class EventEmitter {
class EventHandle {
  constructor() {
    this.$_listeners = {};
  }

  on(name, callback, scope = null) {
    this.$_listeners[name] = this.$_listeners[name] || [];
    if ( scope !== null) callback['_scope'] = scope;
    this.$_listeners[name].push(callback);
  }

  off(name, callback) {
    const listeners = this.$_listeners[name];

    if (Array.isArray(listeners)) {
      const index = listeners.indexOf(callback);
      if (index === -1) return;
      listeners.splice(index, 1);
    }
  }

  trigger(name, params = null) {
    const listeners = this.$_listeners[name];

    if (Array.isArray(listeners)) {
       if (listeners.length <= 0) return;
       listeners.forEach(cb => {
         if (cb && cb instanceof Function) {
           const scope = cb['_scope'];
           if (scope === null) {
             (params === null) ? cb() : cb(params);
           } else {
              (params === null) ? cb.call(scope) : cb.call(scope, params);
           }
         } 
       });
    }
  }

}

Example:

class Hello extends EventHandle {}

function fn1() {
  console.log('fn1');
}
function fn2() {
  console.log('fn2');
}

const hello = new Hello();
hello.on('hello', fn1);
hello.on('hello', fn2);
hello.trigger('hello');
hello.off('hello', fn2);
hello.trigger('hello');

这样就有了 .on() 方法,但是要注意以下两种事件绑定不能被移除的情况

// Bad1:

const hello = new Hello();
hello.on('hello', function() {
    // some code
});
hello.on('hello', function() {
    // another code
});
hello.off('hello', function() {
    // some code;
});

// Bad2:

const hello = new Hello();
hello.on('hello', function fn1() {
    // some code
});
hello.on('hello', function fn2() {
    // another code
});
hello.off('hello', function fn2() {
    // some code;
});

以上代码为什么不能被移除呢?请移步看 匿名函数 函数重复定义 相关的知识点.

实现通过onmessage(e)e.data.eventType 做各业务逻辑控制功能

具体源码

!(function(win) { // win: window
/**
 * 事件处理类
 */
class EventHandle {
  constructor() {
    this.__init();
  }
  
  __init() {
    this._callbacks = {};
  }

  on(name, callback) {
    this._callbacks[name] = this._callbacks[name] || [];
    this._callbacks[name].push(callback);
  }

  off (name, callback) {
    const callbacks = this._callbacks[name];
    if (callbacks && callbacks instanceof Array) {
      const index = callbacks.indexOf(callback);
      if (index === -1) return;
      callbacks.splice(index, 1);
    }
  }

  trigger(name) {
    const callbacks = this._callbacks[name];
    if (callbacks && callbacks instanceof Array) {
      callbacks.forEach(cb => {
        if (cb && cb instanceof Function) cb();
      });
    }
  }
}

/** 
 * @see https://blog.csdn.net/jx950915/article/details/83088349
 * @see https://blog.csdn.net/jx950915/article/details/83111473 
 */
class WSClient extends EventHandle {
  constructor(config) {
    super();
    this.init(config);
  }

  init(config) {
    /*
      websocket接口地址
        1、http请求还是https请求 前缀不一样
        2、ip地址host
        3、端口号
      */
    config = config || {};
    this.config = config;
    const protocol = (window.location.protocol == 'http:') ? 'ws://' : 'wss://';
    const host = window.location.host;
    const port = ':8087';
    //接口地址url
    this.url = config.url || protocol + host + port;
    //socket对象
    this.socket = null;
    //心跳状态  为false时不能执行操作 等待重连
    this.isHeartflag = false;
    //重连状态  避免不间断的重连操作
    this.isReconnect = false;
    /** 是否开启调试-控制台输出 */
    this.isOpenDebug = true;

    this.initEvent();

    //初始化websocket及事件处理
    this.initWs();

  }

  // ================================================================================
  // 初始化事件
  // ================================================================================
  initEvent() {
    //自定义Ws连接函数:服务器连接成功
    this.onopen = ((e) => {
      this.isHeartflag = true;
      
      if (this.isOpenDebug) console.log('服务器连接成功');

      const openCallback = this.config.openCallback;
      if (openCallback && openCallback instanceof Function) {
        openCallback();
      }
    })
    //自定义Ws消息接收函数:服务器向前端推送消息时触发
    this.onmessage = ((e) => {
      //处理各种推送消息
      // console.log(message)
      console.log('接收的消息', e);
      this.handleEvent(e)
    })
    //自定义Ws异常事件:Ws报错后触发
    this.onerror = ((e) => {
      if (this.isOpenDebug) console.log('error')
      this.isHeartflag = false;
      this.reConnect();
    })
    //自定义Ws关闭事件:Ws连接关闭后触发
    this.onclose = ((e) => {
      // this.reConnect()
      if (this.isOpenDebug) console.log('close')
    })
  }

  // ================================================================================
  // websocket 初始化
  // ================================================================================
  /** 初始化websocket连接 */
  initWs() {
    window.WebSocket = window.WebSocket || window.MozWebSocket;
    if (!window.WebSocket) { // 检测浏览器支持  			
      console.error('错误: 浏览器不支持websocket');
      return;
    }
    var that = this;
    this.socket = new window.WebSocket(this.url); // 创建连接并注册响应函数  

    /** 用于指定连接成功后的回调函数 */
    this.socket.onopen = function (e) {
      that.onopen(e);
    };
    /** 用于指定当从服务器接收到信息时的回调函数 */
    this.socket.onmessage = function (e) {
      that.onmessage(e);
    };
    /** 用于指定连接关闭后的回调函数 */
    this.socket.onclose = function (e) {
      that.onclose(e);
      that.socket = null; // 清理  		
    };
    /** 用于指定连接失败后的回调函数 */
    this.socket.onerror = function (e) {
      that.onerror(e);
    }

    return this
  }

  /** websocket 断线重连 */
  reConnect() {
    if (this.isReconnect) return;
    const self = this;
    this.isReconnect = true;
    //没连接上会一直重连,设置延迟避免请求过多
    setTimeout(function () {
      self.initWs()
      self.isReconnect = false;
    }, 2000);
  }

  /** 消息处理 */
  handleEvent(resMsg) {
    if (!resMsg) return;
    let message = resMsg.data;
    const isString = typeof message === 'string';
    let params = {};
    if (isString) {
      // message = JSON.parse(JSON.stringify(message.data));
      // message.data = message.data.replace(/\s+/g, '');

      // TEST: 事件触发测试
      params.data = {};
      params.data.shakeCount = 6;
      params['eventType'] = 'open_shake';
    } else {
      params = message;
    }
    
    if (params && params['eventType']) {
      const action = params['eventType'];
      // const retCode = message.params.retCode.id;
      //根据action处理事件
      if (this._callbacks[action] && this._callbacks[action] instanceof Array) {
        this._callbacks[action].forEach(cb => {
          console.log('参数', params.data);
          if (cb && cb instanceof Function) cb(params.data);
        })
      }
    }

  }

  // ================================================================================
  // 项目部分,不同项目需要调整
  // ================================================================================
  emit(name, data) {
    //ws还没建立连接(发生错误)
    if (!this.isHeartflag) {
      console.log('连接中……')
      return;
    }

    //组装json数据
    const sendData = {
      eventType: name,
      data: data
    }
    if (this.isOpenDebug) console.log('发送消息', sendData);
    this.socket.send( JSON.stringify(sendData) );
  }

}

win.__WSClient__ = WSClient;

})(window);

关键部分在这里

emit(name, data) {
    //ws还没建立连接(发生错误)
    if (!this.isHeartflag) {
      console.log('连接中……')
      return;
    }

    //组装json数据
    const sendData = {
      eventType: name,
      data: data
    }
    if (this.isOpenDebug) console.log('发送消息', sendData);
    this.socket.send( JSON.stringify(sendData) );
}

在使用过程中就能这样:

wsclientInstance.emit('hello', { msg: 'Hello Everyone!!!' })
wsclientInstance.on('hi', function(message) {
    // 服务端发送消息 onmessage(e) 时 e.data.eventType 为 'hi' 的消息,然后进行处理
})

遇到问题处理

  • 多次 JSON.strinify() 会怎么样?

前端接收后端(ws 开启的 WebSocket服务)发现出现了 / 和 多层 "" 的问题,下面是造成这个问题的一个原因的处理(其他原因这里不做展开分析)。

    
!(function () {
let obj = {
  x: 1
}

const str1 = JSON.stringify(obj);

console.log( str1 );
// => {"x":1}

const str2 = JSON.stringify(str1);

console.log( str2 );
// => "{\"x\":1}"

const str3 = JSON.stringify(str2);

console.log(str3);
// => "\"{\\\"x\\\":1}\""

const result1 = JSON.parse(str3);
const result2 = JSON.parse(result1);
const result3 = JSON.parse(result2);

// const result4 = JSON.parse( result3 );
// Uncaught SyntaxError: Unexpected token o in JSON at position 1
// 因为 result3 已经为一个对象了.

console.log( 'result1', result1 ); 
// => result1 "{\"x\":1}"
console.log( 'result2', result2 ); 
// => result2 {"x":1} 
console.log( 'result3', result3 );
// => result3 {x: 1} 对象
// console.log( 'result4', result4 );

})();

上面代码会看到多次执行了 JSON.stringify(), 结果每次都不一样。从 str2 开始出现 \, 出现这个问题的时候,有些童靴可能会采取正则表达式 /[\\]/g 进行处理,这是一个方案,但是如果进一步测试发现是多次 JSON.stringify() 造成的,所以也可以采取多次 JSON.parse() 的方式进行处理,但是通过上面的代码会发现: 如果对 object 进行 JSON.parse() 会报错,所以最好先 typeof xxx === 'string' 做下判断后再 JSON.parse()

未完待续...

参考资料