另辟蹊径:如何利用打桩和Mock的思想模拟WebSocket,实现彻底前后端分离独立开发

问题提出

之前的文章里,我介绍了 如何在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 请求,将匹配的请求拦截之后返回开发者准备好的数据。

真正的请求响应
另辟蹊径:如何利用打桩和Mock的思想模拟WebSocket,实现彻底前后端分离独立开发_第1张图片
使用Mockjs拦截并响应
另辟蹊径:如何利用打桩和Mock的思想模拟WebSocket,实现彻底前后端分离独立开发_第2张图片
可以看出,使用Mock的情况下,应用并没有真正向服务器发起请求。

那么我们有没有办法也拦截 WebSocket 请求,然后将我们准备好的数据推送给应用呢?

很不幸,没有办法。至少目前我并没能找到一个在应用端拦截并推送 WebSocket 消息的方法。

虽然我们不能拦截请求,但是从本质上讲,**其实这里我们真正的需求是什么呢?**带着这个问题,我们来看一下,我是如何解决这个问题的。

问题解决

我们本质的需求是,将我们准备好的数据推送给应用,而不真正发起请求,此时,其实我们可以使用 打桩 的思路来解决。

这里顺便提一下,我们前端工程化开发之后,摸索出了一个最佳实践,就是将所有后台请求都封装起来放在api文件夹下,然后将所有的请求路径都放在 api-paths.js 中:
另辟蹊径:如何利用打桩和Mock的思想模拟WebSocket,实现彻底前后端分离独立开发_第3张图片
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” 就可以(注意引号,而且这个值的修改需要重启应用)。
在这里插入图片描述

另辟蹊径:如何利用打桩和Mock的思想模拟WebSocket,实现彻底前后端分离独立开发_第4张图片

你可能感兴趣的:(Vue学习笔记,单元测试,WebSocket,Mock)