最近用 http://socket.io 写游戏的时候,发现怎么写怎么不爽。发送消息到不是什么事儿,但是接受消息就怎么用怎么不爽。试想一个场景,假设我向某个端请求一份数据,并且要求这个请求给我一份相应,而且一定要把这请求和响应一一对应,逻辑上不能打断。这直接用 http://socket.io 的 emit / on 的方式就感觉坑真多。前端向后端发送一份请求,并要求一份响应的时候还好,不是有 Ajax 嘛?后端向前端发送一份请求并要求一份响应呢?
于是,劳动人民就要发动劳动人民伟大的智慧,造了这么一个玩意儿 bramblex/io-request 。我给他取名叫做 IORequest,用来后端向前端请求一份数据并返回响应。嗯,请不要吐槽我取名字的能力。
IORequest 是如何设计的
IORequest 分为服务端和客户端两个部分,但是这两个部分都只监听并且发送,两个事件: io-request 和 io-response 。
流程大致是,A 端首先生成一个 message_id ,然后向 B 端发送 io-request 事件,并且附上 message_id,并且返回一个 promise。然后 B 端处理完各种东西以后向 A 端发送 io-resoponse 事件,同时附上从 io-request 上接受到的 message_id。最后,A 端接收到 io-response 事件,并通过 message_id 来 resolve 或者 reject 最先返回的 promise。【注】在这个过程中我唯一遇到的坑就是 Promise 这货不能在外部 resolve 或者 reject,而且在 node 里面还不能继承。所以只能通过一些 “奇技淫巧” ,通过外部的引用来把 resolve 和 reject 导出来,并给 promise 对象添加上。
如何使用
class Game {
constructor (left, right) {
co(function *() {
const home_id = next_home_id++
left.emit(`msg`, `游戏开始,游戏房间号[${home_id}],等待你出拳`)
right.emit(`msg`, `游戏开始,游戏房间号[${home_id}],等待你出拳`)
// 这里就是向前端请求 choice 方法,并且等待前端的返回 const [l, r] = yield [
left.ioRequest({method: 'choice'}),
right.ioRequest({method: 'choice'})
]
// 后端接受到前端的返回后继续游戏的进程 const _choices = [null, '布', '剪刀', '石头']
left.emit('msg', `你出的是[${_choices[l]}], 对方出的是[${_choices[r]}]`)
right.emit('msg', `你出的是[${_choices[r]}], 对方出的是[${_choices[l]}]`)
// 比较并且得出结果 if (Game.compare(l, r)) {
left.emit('msg', '你赢了')
right.emit('msg', '你输了')
} else {
right.emit('msg', '你赢了')
left.emit('msg', '你输了')
}
// 游戏结束,关闭链接 right.disconnect()
left.disconnect()
})
}
}
应为游戏逻辑都在后端处理了,那么前段就根本不需要再处理更多逻辑,而只需要负责处理后端的请求,并且显示后端推送的信息便可。
// 前端在这里只需要处理后端发送的 choice 请求,并且处理完后通过 response 返回请求的数据 ioReqClient.handle('choice', ({response}) => {
const choices = document.getElementById('choices')
choices.style.display = 'block'
const shi = document.getElementById('shi')
const jian = document.getElementById('jian')
const bu = document.getElementById('bu')
// 当按钮被点击的时候 shi.onclick = () => {
response(3) // 在这里给后端返回 choices.style.display = 'none'
msgLog('我出的是[石头]')
}
// 同上 jian.onclick = () => {
response(2)
choices.style.display = 'none'
msgLog('我出的是[剪刀]')
}
// 同上 bu.onclick = () => {
response(1)
choices.style.display = 'none'
msgLog('我出的是[布]')
}
})
最后放一张游戏的截图:
打开两个客户端会自动分配一个游戏房间:
当两个人都出了拳以后:
IORequest 解决了什么问题?
有些小伙伴问题,这个东西他到底有什么用呢?除了增加耦合以外?
嗯,当然是有用的啊,但是我这不叫做增加 “耦合” 而叫做 “聚合”。我们把不应该放在一起的事情放在一起做了,这就叫做耦合。但是我们把零零散散逻辑上本该一起做的东西放在一起了,这叫做聚合。所谓我们学习软件工程课程的时候常常听到一句话叫做 “高聚合,低耦合”。
IORequest 把零散的请求和相应整合到了一起,这就叫做聚合。而上面例子的 Game 类,把游戏运行逻辑和消息的发送放在了一起这叫做耦合。但是上面的 Game 类仅仅是一个非常简单的例子而已,只会用到一次,当然怎么方便怎么做嘛。
所以,IORequest 的作用,就是将 http://socket.io 的部分需要返回响应和他们做对应的请求整合在了一起而已,因为它们本就是逻辑上不可分割的一部分。