针对 websocket 服务代理与数据 mock 的方案

目录
[隐藏]

在前后端分离的协作开发过程中,mock 数据是一个不可避免的需求。针对普通 http 方式的数据mock有非常多的成熟解决方案,但面向 websocket 这种推送模式就变得复杂一些。

下面分享一下借助 simple-mock 插件库实现 websocket 服务的数据请求 mock 方案。

1 simple-mock 简介

在无需产品和后端等其他角色参与、仅为前端开发提供便利的情况下,我个人在开发过程中更倾向于不入侵前端项目又简易轻量的数据 MOCK 方案,于是基于工作中前端业务的开发模式,开发了 simple-mock 插件库。 simple-mock 所提供的方案主要基于以下需求点:

  • 可以注入 webpack-dev-server(Vue-cli、Angular-cli) 或 express、Koa 等已有 server 中
    • 可以代理后端服务,代理过程中可以保存接口返回的结果作为基础 mock 数据
    • js/ts 文件方式编写 commonjs/esModule 风格的代码来自定义 mock 规则
  • 前端开发可以尽量少的配置来开启 mock 模式
  • 可以实时加载配置文件、自定义 mock 文件
  • 可以定义公共规则,设计不同的优先级
  • 可以针对部分接口进行 mock
  • more…

1.1 可以注入到已有 Server 中

simple-mock 的初始目的即不改变现有的架构模式,又能实现 Server 级的 Mock 功能。所以 simple-mock 主要通过实现并导出 simpleMock.rendersimpleMock.saveApi 两个方法给用户调用,在内部实现具体的 mock 逻辑框架:

  • simpleMock.render 方法实现 mock 功能。它应在接口请求时注入,根据配置文件规则决定是否 mock 当前请求、如何处理当前请求。具体的 mock 规则都是以 js/ts 文件方式编写前端最熟悉的 commonjs 代码来实现,没有复杂的规则需去掌握,对前端开发者来说学习成本几乎为零。
  • simpleMock.saveApi 方法实现自动保存服务端接口返回的数据功能。它应在服务端 API 代理数据返回时调用,根据配置文件规则决定如何保存返回的数据。当前的各种 mock 方案中,对前端人员的自行 mock 规则制定都有所要求,当接口变动时已有的规则也需要进行修正。这里通过代理模式自动保存后端返回数据的方式减少手工 mock 规则制定,在开启 mock 模式时,只关注当前需处理的接口。

对于其他的需求点,在存在项目业务差异化的地方均通过配置文件编写配置参数的方式进行实现。

1.2 零配置/尽量少的配置来开启 mock 模式

当前各种流行的 mock 方案中,因其功能的丰富性,对前端人员的自行 mock 规则制定都有所要求,在已有项目中引入的初始成本相对会比较高,也存在一定的学习成本。

simple-mock 通过导出 simpleMock.saveApi 方法,在服务端 API 代理数据返回时给用户调用,根据配置文件规则保存返回的数据。
通过代理模式自动保存后端返回数据的方式,减少了手工 mock 规则制定,使得引入成本相对较低。只需要稍微编写一些公共的修正规则(如无特殊的接口逻辑甚至这一步可以省略),即可快速实现整个站点的完整 MCOK 开发。

在 MOCK 开发过程中,只需关注当前处理的业务相关数据接口,针对性的编写 Javascript 逻辑实现自定义数据 MOCK。

1.3 实时加载配置文件、自定义 mock 文件

配置文件与 mock 文件都是作为 nodejs 的模块 require 进来的,因为 require 机制存在缓存,从而导致文件修改后只能重启才能生效,而作为插件只提供 API 的 simple-mock 显然没有重启 server 的能力。

在本地开发模式下对内存消耗的要求并不用太高,所以 simple-mock 在内部实现了简单的缓存机制,当发现相关文件被修改时,使用delete require.cache[filename] 的方法移除缓存(注意,这种方式可能会导致内存泄露,除非有很好的对象释放控制逻辑,不建议在生产环境中应用),从而实现文件修改后再次 require 立即生效的效果。

1.4 定义公共规则,设计不同的优先级

simple-mock 共设置三个目录用于定义 mock 规则文件,其优先级从高到低依次为: customdata > mockdata > customdata/autosave

  • customdata 目录用于自定义 API 规则,不会提交至 GIT 仓库。该目录优先级最高
  • mockdata 目录用于存放常用的公共 API 规则,会提交至 GIT 仓库
  • customdata/autosave 目录用于自动保存后端返回数据,也是保底的 mock 规则

1.5 针对部分接口进行 mock

当关闭 mock 功能(设置config.isEnableMock=false)时,config.enableMockFilter 参数方法可以用于定义仍可开启的规则。这对于只对单一接口进行不同参数值进行 MOCK 调试的场景非常有用。示例:

module.exports = {
    enableMockFilter: (apiPath, req) => {
        // 示例:按URL 路径匹配,开启部分接口的 mock
        const filterKeyList = ['/rest/test', '/rest/abc'];
        const isMock = filterKeyList.some(path => String(apiPath).includes(path));
        return isMock;
    },
}

当开启 mock 功能(设置config.isEnableMock=true)时,config.disableMockFilter 参数方法可以用于定义仍不用 mock 的规则。这对于在 mock 开发情况下,需要调试或比对部分线上接口的场景很有帮助。示例:

module.exports = {
    disableMockFilter: (apiPath, req) => {
        // 示例:按URL关键字过滤,登陆相关API不mock
        const filterKeyList = ['/rest/auth', '/rest/logout'];<img src="https://lzw.me/wp-content/uploads/2020/07/websocket-mock-server-140x100.png" alt="" width="140" height="100" class="aligncenter size-thumbnail wp-image-2542" />

<img src="https://lzw.me/wp-content/uploads/2020/07/websocket-mock-msg-send-2-140x100.png" alt="" width="140" height="100" class="aligncenter size-thumbnail wp-image-2541" />
        const isMock = filterKeyList.some(path => String(apiPath).includes(path));
        return isMock;
    },
}

当然,开启了 mock 开关时,也有多种方式终止 mock 流程。例如 在 config.customSaveFileName 参数方法中匹配相关规则并将 filename 返回空、在config.handlerBeforeMockSend 参数方法中匹配相关请求并返回 __ignore_mock__、在该请求对应的自定义文件中导出值为 __ignore_mock__等。

上面简单介绍了 simple-mock 的主要目的与设计思路。面向 http 一问一答的请求方式,simple-mock 可以很好的解决轻量型 mock 开发模式需求。

2 使用 simple-mock 实现 websocket 服务 mock 的思路与方案

对于 websocket 这种订阅与推送模式,其实同样可以复用 simple-mock 在面向 http 一问一答的 mock 思路,只是不同的地方在于,不同订阅请求的数据如何保存、订阅一次后如何模拟多次数据的发送等。

2.1 simple-mock 处理 websocket 消息 mock 的基本策略

一般来说,除了服务器广播模式推送的数据外,对于发送请求则需等待数据返回的普通模式,因为需要区分不同的数据应答,websocket 的客户端数据发送与服务器数据返回之间基本都是需要根据某些字段值进行关联的,基于这种关联规则,可以通过配置参数的方式去实现 mock 文件名自定义、数据通用处理等实现 mock 逻辑,在具体的 mock 框架上基本保持不变。simple-mock 针对这种逻辑的主要点有:

  • 借助 simple-mock 以一问一答、一问多答的模式实现 websocket 服务 mock 逻辑:
    • 通过 config.customSaveFileName(req, res, filename, type) 参数方法定义客户端数据请求、服务端数据返回的 mock 文件名规则,由此实现应与答的对应。
    • 通过 config.handlerBeforeMockSend(content, reqParams) 参数方法对通用信息进行调整,比如实时时间戳、与请求对应的 uuid 替换等。
  • 对于登陆流程、信息预加载等,定义公共规则,放置于 mock/mockdata 目录中(会提交至git仓库)
  • 对于广播推送式消息,可以定义公共规则、定义定时器等方式。建议在启动连接后,通过开启本地 websocket mock server 页面进行手动模拟消息并发送

2.2 一问一答、一问多答模式

在具体的业务流程里,一问一答、一问多答模式还是比较多的,也是比较主要的模式,例如发送一条消息,服务器对应返回一条或多条消息。

发送与返回的消息需要进行对应,这里面一般会在每次的应答中设置一个 uuid 唯一标识字段用于匹配。config.handlerBeforeMockSend 参数即可用于处理这种公共逻辑关系,config.customSaveFileName 参数则用于处理请求与本地文件之间的对应关系。

一应一答模式自然比较简单,autosave 目录落地的数据即可满足基本需求。一应多答模式则需要以编写函数的方式具体的规则来实现。以下是一个一应多答的自定义示例:

/**
 * 自定义 MOCK 逻辑示例:查询并要求返回多条信息
 */
const path = require('path');
module.exports = (req, client) => {
  const jparams = req.data && req.data.jparams;
  const reqmsgid = jparams && params.reqmsgid;
  const totalCounts = result.data.totalCounts;
  const funcid = result.data.funcid;
  // reqmsgid 是本次请求的 uuid 唯一标识,用于应答匹配
  // 这种公共约定也可以在 config.handlerBeforeMockSend 参数中进行实现
  result.data.reqmsgid = reqmsgid;

  // 将多条保存至本地的数据
  if (totalCounts) {
    for (let i = 0; i < totalCounts; i++) {
        const filePath = path.resolve(__dirname, `../customdata/autosave/${funcid}_currSno${i}.js`);

        if (fs.existsSync(filePath)) {
            let data = require(filePath);
            data.data.reqmsgid = reqmsgid;
            // 也可以调用 config.handlerBeforeMockSend 进行一下通用处理再发送
            // const CONFIG = require('../../simple-mock-config');
            // data = CONFIG.handlerBeforeMockSend(data, req, client);

            setTimeout(() => {
                client.broadcast(data);
                // client.send(data);
            }, 1000);
        }

    }
  }
  if (jparams && jparams.market) {
      let s = jparams.market;
      if (s === '1' && jparams.filterstktypes) s += '_filterstktypes';
      const filePath = path.resolve(__dirname, `../customdata/autosave/10001_${s}.js`);

      if (fs.existsSync(filePath)) {
          const data = require(filePath);
          // console.log('data', data);
          setTimeout(() => {
              client.broadcast(data);
              // client.send(data);
          }, 100);
      }
  }
  // 返回 false 则忽略自动应答,在内部自行处理
  // return result;
  return false;
};
// 示例基础数据
const result = {
  data: {
    msg: '信息查询成功!',
    errno: 0,
    data: [],
    currSno: 0,
    totalCounts: 10,
    funcid: 10001,
    reqmsgid: '2da60eda555000000000000000000000',
  },
  topic: 'ask',
};

该示例展示了一次请求需要多次信息返回的场景。其中 client 是在调用 simpleMock.renderWs 方法时传入的第二个参数,可以直接传入 ws 客户端句柄,也可以自行实现其相关接口实现更多的自定义能力。

2.3 登陆流程、信息预加载

在登入系统初始化流程中,可能需要加载比较多的信息。这些信息的接收逻辑可以写为公共规则固定下来,将编写好的公共规则放置于 mock/mockdata 目录中,则会提交到 Git 仓库以便与所有开发者共享。

此外,对于一些公共性的信息加载,可能也需要编写公共规则以让应用能够正常运行。

2.4 Mock 推送式消息

对于无请求、纯推送方式的消息,一种简单的方式是在启动时开启一个定时器(setInterval/setTimeout),定时的发送消息给客户端(注意对定时器的管理,避免造成内存泄露)。例如定时读取指定目录下的 json 文件内容作为消息发送。只需要删除文件或修改文件内容即可满足简单的需求。

在开发过程中,对于推送式消息进行 Mock 的主要目的是用于功能调试,所以开启一个网页客户端来手动发送 mock 数的方式更为便捷。如下图示例:

通过网页客户端进行任意消息的即时推送可以很方便的满足各种数据场景的开发调试需要。在 simple-mock 仓库的 ws-proxy-server/mock-client.ts 中给出了这种模式的一种实现示例。

以上即实现了对 websocket 服务基本的 mock 逻辑。在 simple-mock 源码仓库 ws-proxy-server 目录下有一个具体的例子可供参考。

2.5 其他

  • 如不关注日志信息,可以在配置文件中开启 simple-mockws-proxy-server 的日志打印开关参数 slient。因为大量的日志打印可能会导致 cmd 出现卡顿或进程卡住需手动回车才会继续的现象。
  • more…

相关链接参考

点赞 (0)

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据