前言:之前有个项目需要使用 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()
未完待续...