之前的文章里,我介绍了 如何在Vue项目中使用Mockjs,模拟接口返回的数据,实现前后端分离独立开发 ,而且也解决了Mockjs如何拦截带参数的GET请求 的问题。
最近接到一个开发客服IM的任务,需要用到 WebSocket 做前后端实时消息推送,在做页面的时候发现页面的http请求可以使用Mockjs来拦截并返回mock出来的数据,但是 WebSocket 却很难做到。于是查了很多资料,但是基本都是需要依赖独立的后台服务,比如专门用于测试的稳定后台Server环境,或者 MockServer 一类的。
但是本着简单直接的解决方式,如果有一个办法可以像Mockjs那样独立、便捷就好了。于是思考了几天。偶然的机会,我看到一篇 infoQ 的文章, Stubbing, Mocking and Service Virtualization Differences for Test and Development Teams 这篇文章讲的是打桩、mock和服务虚拟化之间的关系和使用场景,虽然没有直接给出什么解决方案,但是由此给了我灵感。WebSocket 的模拟问题,其实可以从更本质的角度去思考。
从 Mockjs 的原理,我们知道,它是通过捕捉应用发出的 http 请求,将匹配的请求拦截之后返回开发者准备好的数据。
真正的请求响应:
使用Mockjs拦截并响应
可以看出,使用Mock的情况下,应用并没有真正向服务器发起请求。
那么我们有没有办法也拦截 WebSocket 请求,然后将我们准备好的数据推送给应用呢?
很不幸,没有办法。至少目前我并没能找到一个在应用端拦截并推送 WebSocket 消息的方法。
虽然我们不能拦截请求,但是从本质上讲,**其实这里我们真正的需求是什么呢?**带着这个问题,我们来看一下,我是如何解决这个问题的。
我们本质的需求是,将我们准备好的数据推送给应用,而不真正发起请求,此时,其实我们可以使用 打桩 的思路来解决。
这里顺便提一下,我们前端工程化开发之后,摸索出了一个最佳实践,就是将所有后台请求都封装起来放在api文件夹下,然后将所有的请求路径都放在 api-paths.js 中:
api-paths.js 文件的内容类似这样:
let wx_server = process.env.WX_SERVER;
let apiPaths = {
websocket: {
endpoint: wx_server + "/ws",
path: {
subMsg: "/user/topic/subNewMsg"
}
},
chat: {
getChatList: wx_server + "/chat/getChatList",
send: wx_server + "/chat/send",
}
}
export default apiPaths
而每一个 *-api.js 文件模块负责一类api请求。这里,我的 ws-api.js 负责所有的 WebSocket 连接和订阅消息的任务。
话外音:我在项目中使用了STOMP over SockJS,我发现STOMP的热度远WebSocket高,真是让人费解。
我们假设HTTP协议并不存在,只能使用TCP套接字来编写Web 应用。你可能认为我已经疯掉了……幸好我们有HTTP……大多数的开发人员并不需要编写低层级TCP套接字通信相关的代码。直接使用WebSocket(或SockJS)就很类似于使用TCP套接字来编写Web应用……好消息是我们有STOMP……用来定义消息的语义。 ——《Spring实战》
ws-api.js (与主题有关的部分代码)
/**
* 建立websocket连接
* @param {Function} onConnecting 开始连接时的回调
* @param {Function} onConnected 连接成功回调
* @param {Function} onError 连接异常或断开回调
*/
function connect(onConnecting, onConnected, onError) {
onConnecting instanceof Function && onConnecting();
let sock = new SockJS(ApiPaths.websocket.endpoint);
client = Stomp.over(sock);
client.connect({}, function(frame) {
onConnected instanceof Function && onConnected();
}, function(err) {
console.warn("与服务器断开连接," + nextTime + " 秒后重新连接", err);
setTimeout(() => {
console.log("尝试重连……");
connect(onConnecting, onConnected, onError);
}, 5000);
onError instanceof Function && onError();
});
}
/**
* 订阅新消息,就算连接还未建立也可以,程序会记录订阅情况,在连接建立后再次订阅
* @param {Function} cb 回调
*/
function subNewMsg(cb) {
client.subscribe(ApiPaths.websocket.path, function(resp) {
console.debug("ws收到消息: " + resp.body);
cb instanceof Function && cb(resp.body);
});
}
export default {
connect,
subNewMsg
}
如果你有后端开发经验的话,面向接口编程 的思想应该非常熟悉。这里,我们可以把 ws-api.js 看成一个接口,这个接口定义了connect(onConnecting, onConnected, onError) 和 subNewMsg(cb) 两个方法。那么,我们其实可以写一个用于模拟的实现类,即使用实现同样方法的类(替身)来代替原来的类,这个替身是为模拟用的,因此我们可以实现任何我们期望的操作。
ws-api-mock.js
// websocket api 打桩
const Mock = require("mockjs");
function connect(onConnecting, onConnected, onError) {
// 模拟正在连接
onConnecting && onConnecting();
setTimeout(() => {
// 模拟连接成功
onConnected && onConnected();
}, 1000)
}
function subNewMsg(cb) {
// 每 3 秒使用Mock出来的数据做为参数调用一次回调函数模拟接收到 WebSocket 推送的消息
setInterval(() => {
cb(Mock.mock({
appId: "11",
openId: /sisf|dsdf|anb2|sss4|safs4|sd22|bbas|sss/,
"msgId|1-10000000": 1,
content: '@csentence', //聊天内容
sendType: /REC|SEND/,
"createTime|1543800000-1543851602": 1, //时间
msgType: "text",
}));
}, 3000);
}
export {
connect,
subNewMsg
}
这里我们模拟了连接成功的场景并且每 3 秒使用Mock出来的数据做为参数调用一次回调函数模拟接收到 WebSocket 推送的消息。
用这个ws-api的替身,拥有与ws-api相同的方法,因此是相互兼容的,那么什么时候用,怎么使用它呢?而且如何区分线上环境和开发环境呢?
还是与引用Mockjs相同的思路,我们在 dev.env.js
环境配置中添加 MOCK: "true"
这个配置项,然后修改 ws-api.js 文件的 export 语句。当开启Mock时,我们使用替身,否则使用真身。
let mock;
if (process.env.MOCK) {
mock = require('../../mock/ws-api-mock')
}
export default mock || {
connect,
subNewMsg
}
使用了替身之后,就算后台服务器并没有开启,我们也照样可以连接成功,并且接收到推送过来的消息。而因为我们只在 dev.env.js
环境配置中添加 MOCK: "true"
因此,在编译时会自动替换成真身,而不用做额外的设置。而且如果你想在开发时关闭mock的话,也只需要把 MOCK 变量设置为 “false” 就可以(注意引号,而且这个值的修改需要重启应用)。