看了socket.io emit的代码实现后,思路也是大同小异,通过将当前emit的事件名和参数按一定规则组合成数据,然后将数据通过WebSocket的send方法发送出去。接收端按规则取到事件名和参数,然后本地触发emit。(注意远程emit和本地emit,socket.io中直接调用的是远程emit)。
下面是实现代码,事件直接用的emitter模块,并且为了能自定义emit事件名和参数组合规则,以中间件的方式提供处理方法:
export default class EasySocket extends Emitter{//继承Emitter constructor(config) { this.url = config.url; this.openMiddleware = []; this.closeMiddleware = []; this.messageMiddleware = []; this.errorMiddleware = []; this.remoteEmitMiddleware = [];//新增的部分 this.openFn = Promise.resolve(); this.closeFn = Promise.resolve(); this.messageFn = Promise.resolve(); this.errorFn = Promise.resolve(); this.remoteEmitFn = Promise.resolve();//新增的部分 } openUse(fn) { this.openMiddleware.push(fn); return this; } closeUse(fn) { this.closeMiddleware.push(fn); return this; } messageUse(fn) { this.messageMiddleware.push(fn); return this; } errorUse(fn) { this.errorMiddleware.push(fn); return this; } //新增的部分 remoteEmitUse(fn, runtime) { this.remoteEmitMiddleware.push(fn); if (runtime) { this.remoteEmitFn = compose(this.remoteEmitMiddleware); } return this; } connect(url) { ... //新增部分 this.remoteEmitFn = compose(this.remoteEmitMiddleware); } //重写emit方法,支持本地调用以远程调用 emit(event, args, isLocal = false) { let arr = [event, args]; if (isLocal) { super.emit.apply(this, arr); return this; } let evt = { event: event, args: args } let remoteEmitContext = { client: this, event: evt }; this.remoteEmitFn(remoteEmitContext).catch(error => { console.log(error) }) return this; } }下面是一个简单的处理中间件:
client.remoteEmitUse((context, next) => { let client = context.client; let event = context.event; if (client.socket.readyState !== 1) { alert("连接已断开!"); } else { client.socket.send(JSON.stringify({ type: 'event', event: event.event, args: event.args })); next(); } })意味着调用
client.emit('chatMessage',{ from:'admin', masg:"Hello WebSocket" });就会组合成数据
{ type: 'event', event: 'chatMessage', args: { from:'admin', masg:"Hello WebSocket" } }发送出去。
服务端接受到这样的数据,可以做相应的数据处理(后面会使用nodejs实现类似的编程模式),也可以直接发送给别的客户端。客户受到类似的数据,可以写专门的中间件进行处理,比如:
client.messageUse((context, next) => { if (context.res.type === 'event') { context.client.emit(context.res.event, context.res.args, true);//注意这里的emit是本地emit。 } next(); })如果本地订阅的chatMessage事件,回到函数就会被触发。
在vue或react中使用,也会比之前使用路由的方式简单
mounted() { let client = this.$wsClients.get("im"); client.on("chatMessage", data => { let isSelf = data.from.id == this.user.id; let msg = { name: data.from.name, msg: data.msg, createdDate: data.createdDate, isSelf }; this.broadcastMessageList.push(msg); }); }组件销毁的时候移除相应的事件订阅即可,或者清空所有事件订阅
destroyed() { let client = this.$wsClients.get("im"); client.removeAllListeners(); } 心跳重连核心代码直接从websocket-heartbeat-js copy过来的(用npm包,还得在它的基础上再包一层),相关文章 初探和实现websocket心跳重连。
核心代码:
heartCheck() { this.heartReset(); this.heartStart(); } heartStart() { this.pingTimeoutId = setTimeout(() => { //这里发送一个心跳,后端收到后,返回一个心跳消息 this.socket.send(this.pingMsg); //接收到心跳信息说明连接正常,会执行heartCheck(),重置心跳(清除下面定时器) this.pongTimeoutId = setTimeout(() => { //此定时器有运行的机会,说明发送ping后,设置的超时时间内未收到返回信息 this.socket.close();//不直接调用reconnect,避免旧WebSocket实例没有真正关闭,导致不可预料的问题 }, this.pongTimeout); }, this.pingTimeout); } heartReset() { clearTimeout(this.pingTimeoutId); clearTimeout(this.pongTimeoutId); } 最后源码地址:easy-socket-browser
nodejs实现的类似的编程模式(有空再细说):easy-socket-node
实现的聊天室例子:
聊天室前端源码:lazy-mock-im
聊天室服务端源码:lazy-mock