WEBSOCKET MOCK SERVER 实现方案与应用


2020/06

目录概览


  • 一、为什么需要 MOCK
  • 二、为什么要自研发
  • 三、Websocket Mock 方案设计
  • 四、典型用法示例
  • FAQ

一、为什么需要 MOCK

一、为什么需要 MOCK


  • 约定好接口,前后端协同并发
  • 避免开发环境问题长时间阻塞前端开发调试
  • 测试场景还原
  • 接口更新、接口文档维护、测试用例等(复杂需求,如 RAP2)
  • More...

社区中常见的 MOCK 实现

  • 在业务逻辑中临时写假数据
  • 前端 js 库拦截请求(mock.js)
  • 利用 Charles、 Fiddler 等代理工具拦截请求
  • 开发简单的 MOCK SERVER
  • 功能丰富的 MOCK SERVER
    • 功能:前端开发、文档管理、测试用例等
    • 多角色:产品、后端、前端、测试等
  • More...

二、为什么要自研发

二、为什么要自研发


纯前端开发的MOCK基本诉求:

  • 不入侵业务逻辑
  • 简易、学习成本低
  • 引入成本低
  • 弱关注后端接口、文档等状态
  • More...

二、为什么要自研发


  • 简单方案不满足需求
  • 复杂方案引入成本、学习高
  • 流行的开源 Mock Server 方案学习成本高
  • 没有适合 websocket 的开源 Mock 方案
  • 定制更符合前端团队当前研发需求的轻量级方案
  • More

三、Websocket Mock 方案设计与开发模式


  • 基本架构设计
  • 主要功能特色
  • Mock 的基本策略

Websocket Mock - 主要功能特色

  • 学习成本低

    commonJS 编写 Mock 规则

  • API 录制

    落地后端返回数据到本地文件

  • 不同的目录优先级

    customdata > mockdata > customdata/autosave

  • 部分开启或关闭 MOCK

    [http完整/websocket部分支持]

  • 修改立即生效

    配置文件、自定义 mock 文件

  • More...

Websocket Mock - 针对 websocket 消息 mock 的基本策略


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

四、典型应用示例

示例 - 使用 funcid 值作为默认文件名

对于 websocket 类型的 mock,主要通过对请求参数与数据内容的字段值进行对应,customSaveFileName 参数是实现这种对应关系的核心。

// simple-mock-config.ts
module.exports = {
  isEnableMock: false,
  customSaveFileName: (reqParams, resData, filename, type) => {
    console.log(type, filename);
    if (filename) return filename;
    if (reqParams.topic) filename = reqParams.topic;

    if (type === 'save' && resData) {
      filename = resData.funcid;
    } else if (type === 'mock' && reqParams) {
      filename = reqParams.funcid;
    }
    return filename;
  }
} as SimpleMockConfig;

示例 - 使用 handlerBeforeMockSend 重置通用数据值

// simple-mock-config.ts
module.exports = {
  isEnableMock: true,
  handlerBeforeMockSend: (content, reqParams, wsClient) => {
    if (reqParams.reqmsgid && content.data.reqmsgid) {
      content.data.reqmsgid = reqParams.reqmsgid;
    }
    if (content && content.data && content.data.data) {
        if (content.data.data.totalCounts) {
            content.data.data.currSno = 0;
            content.data.data.totalCounts = 1;
        }
    }
    return content;
  },
} as SimpleMockConfig;

通过灵活定义 handlerBeforeMockSend 参数,可以在数据返回前统一处理一些参数值,如模拟当前时间、通用性规则等。

示例 - 过滤部分请求的响应

// simple-mock-config.ts
module.exports = {
  isEnableMock: true,
  /** 自动保存 API 返回内容时,对内容进行过滤,返回 true 则忽略 */
  fnAutosaveFilter: (content) => {
      // 示例: 不保存 content.data.length = 0 的数据
      if (content && Array.isArray(content.data) && !content.data.length) {
        return false;
      }
      return true;
  },
  /** 开启 mock 时,过滤一些不需要 mock 的 API */
  disableMockFilter: (filename, req) => {
      // 示例:按关键字过滤部分请求不 mock
      const filterKeyList = ['30001', '30002'];
      return filterKeyList.some(key => String(filename).includes(key));
  },
} as SimpleMockConfig;

示例 - 模拟多次应答

const { dataFormat } = require('../helper/utils-date');

module.exports = (req, ws) => {
  setTimeout(() => {
    loadingFinished.data.mtime = dataFormat('yyyyMMddhhmmssS', new Date());
    // 模拟通知数据已经发送完了
    ws.broadcast(loadingFinished); // ws.send(loadingFinished);
  }, 1000);

  // 响应本次请求
  return result;
};
// 登陆数据预加载:通知数据全部加载完成
const loadingFinished = {
  data: {
    data: [{ remark: 'loading.finished' }],
    mid: 1979, mtype: '1', subid: 1, mtime: '20200520174135',
    topic: 'loading.finished',
  },
  topic: 'gfpb.pub',
};
const result = {...};

示例 - 模拟多次应答

// customdata/44050.js
module.exports = (req, ws) => {
  const jparams = req.data && req.data.jparams;
  if (jparams && jparams.market) {
    const path = require('path'); const fs = require('fs'); const funcid = jparams.funcid;
    let market = jparams.market;
    if (market === '1' && jparams.filterstktypes) market += '_filterstktypes';
    const filePath = path.resolve(__dirname, `../customdata/autosave/proto_base_info_${funcid}_${market}.js`);
    if (fs.existsSync(filePath)) setTimeout(() => ws.broadcast(require(filePath)), 2000);
  }

  return result;
};
const result = {
  data: {
    data: {
      errmsg: '查询证券信息成功!', funcid: 44050, reqmsgid: '2da60eda55594dc9914a780753955a71',
    },
    topic: 'ans_kams',
  },
  topic: 'gfpb.pub',
};

示例 - 使用 MOCK CLIENT 发送消息


示例 - 使用 MOCK CLIENT 发送消息


其他注意事项或经验


  • 灵活定义 config.customSaveFileName 逻辑,落地更多差异化的文件数据
  • 灵活定义 config.handlerBeforeMockSend 处理或修正更多通用性的数据
  • slient: true 关闭日志输出 (大量日志打印cmd偶现卡住情况)
  • 支持 TypeScript 方式编写配置文件,但建议使用 js 方式(修改后编译内存溢出概率高)
  • More...