手动编写mock服务(ma-mock)

作者: 天驱丶 | 来源:发表于2018-07-22 11:18 被阅读1次

    上一篇文章json-server的实践与自定义配置化提到过,json-server在我看来不太适用;之前有赞开源的zan-proxy我也尝试用过,其痛点在于mock数据保存在第三方,这个特性使得公司项目不适合使用zan-proxy,所以尝试自己搭建一个mock服务——ma-mock

    背景

    项目中需要对两个开发地址进行代理,部分数据也需要使用mock数据,所以可以参照zan-proxy做代理和mock的切换按钮,鉴于之前是使用koa编写后端服务的,所以这次使用koa编写可用于mock和proxy的可视化服务

    构思

    虽说是参照zan-proxy,但是我还是保留着自己的想法;首先,与zan-proxy不同,zan-proxy使用浏览器插件进行地址代理,其主要目的是用于调试线上页面,但我们只在dev环境使用,使用webpack的proxyTable将后端接口都代理到mock服务,由mock服务统一分发代理还是返回mock数据即可;其次,数据保存在本地,构建一个本地文件增删查改的操作,mock服务只在dev开发中使用,io的损耗其实没有太大的区别;

    后端

    主要有三个功能,分发mock和proxy提供可视化界面的后端接口部署前端资源,因为主要是给前端人员使用,所以维护一份全局变量(lib/Global.js)替代redis

    三个功能的执行顺序为 分发mock和proxy -> 返回单页面资源 -> 可视化界面的后端接口

    分发功能可以利用koa中间件特性:

    'use strict';
    
    const { Logger, fsHandler, Global } = require('../lib/index');
    const axios = require('axios');
    const pathToRegexp = require('path-to-regexp');
    /**
     *
     * @param  {object} options 配置项
     *         {object} options.prefix mock数据的url前缀
     * @return {function}
     *
     */
    module.exports = options => {
      // 进行中间件参数的配置,最终返回一个中间件函数
      let prefix = options.prefix;
    
      // 兼容prefix格式的写法 "/__DEV__/xxx" 或者 "/__DEV__/xxx/"
      if (prefix.lastIndexOf('/') + 1 !== prefix.length) {
        prefix = prefix + '/';
      }
    
      return async function(ctx, next) {
        let curPath = ctx.path;
        // 不符合prefix的接口地址直接跳过
        if (curPath.indexOf(prefix) !== 0) return await next();
    
        let pathArr = curPath.split(prefix);
    
        // 如果prefix之后不再有path则请求不合法
        // 例如:prefix为 __DEV__/pay/但请求路径为http://*/__DEV__/pay/
        if (pathArr.length < 2) {
          ctx.body = '请求路径不合法';
        }
    
        // 判断是否使用MOCK数据
        const find = Global.mockList.find(it => {
          const re = pathToRegexp(it.url);
          return re.test(`/${pathArr[1]}`);
        });
    
        // 规则是mock优先级大于proxy
        if (find && find.enable) {
          ctx.body = handlerMock(find.url.slice(1));
        } else if (Global.enableProxy) {
          ctx.body = await handlerProxySync(`/${pathArr[1]}`, ctx);
        } else {
          ctx.body = '未开启proxy';
        }
        // 此处没有next(),直接返回数据
      };
    
      // 用mock数据
      function handlerMock(filePath) {
        let result = '';
    
        try {
          result = fsHandler.getMockFile(filePath);
        } catch (e) {
          result = e;
        }
        Logger.debug(result);
    
        return { ...result, type: 'MOCK' };
      }
    
      // 后端代理
      async function handlerProxySync(api, ctx) {
        const options = {
          ...ctx.request,
          url: `${Global.currentProxyUrl}/${api}`,
          params: ctx.query,
        };
    
        try {
          const res = await axios(options);
          return { ...res, type: 'PROXY' };
        } catch (e) {
          return {
            message: e.message,
            type: 'PROXY',
          };
        }
      }
    };
    
    

    中间件使用

    // 配置mock
    app.use(
      mockProxy({
        prefix: '__DEV__',
      })
    );
    

    可视化界面的后端接口

    常规后端restful接口,此处略过不讲。

    前端静态资源部署

    因为是开发环境使用,所以不必部署到nginx上,自己编写了基于koa-statickoa-spa-static

    使用方法,配置采用vue打包出来的目录,react可能需要自行按情况修改:

    const spaStatic = required('koa-spa-static');
    // 挂在静态资源
    app.use(
      spaStatic({
        matchReg: /^(?!\/api)/, // 不以"/api"开头的接口地址会返回静态资源
        root: path.join(__dirname, './dist'), // 静态资源目录
        staticReg: /^\/static/, // 前端static资源返回文件,其他返回index.html
      })
    );
    

    前端

    使用element-ui2的组件构造,属于简单的组装,基于自己编写的vue cli模板,使用命令

    vue init masongzhi/vue-template-webpack

    其他

    ma-mock服务的安装和使用

    安装

    npm install -D ma-mock

    在根目录编写.mamockrc.js配置文件

    const path = require('path');
     
    // 默认配置
    module.exports = {
      prefix: '/__DEV__',
      rootPath: path.resolve(__dirname, './data/mock'),
      proxyPath: path.resolve(__dirname, './data/proxy'),
      proxyFilename: 'config.json',
    };
    

    配置webpack proxyTable

    // ...省略
    module.exports = {
      // ...省略
      dev: {
        // ...省略
        proxyTable: {
          // 填写 .mamockrc.js的prefix,默认为'/__DEV__'
          '/__DEV__': {
            target: 'http://localhost:3001', // 接口的域名
            // secure: false,  // 如果是https接口,需要配置这个参数
            changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
          }
        },
      },
    }
    

    package.json添加script命令

    mamock [--port 3001]

    .mamockrc.js的读取

    我们在项目跟目录编写了.mamockrc.js文件,那是怎么在mock服务中读取到这个文件的信息呢,或者说怎样才能将npm包的配置参数写在根目录的文件内。

    我们可以使用rc-config-loader,其他类似包也行,像prettier就使用editorconfig和自己编写的editorconfig-to-prettier

    const rcfile = require("rc-config-loader");
    
    // 会向上遍历.mamockrc or .mamockrc.json or .mamockrc.js or.<product>rc.yml, .mamockrc.yaml
    // 我们需要用到基于跟目录的data文件,所以需要用到__dirname,所以使用js文件
    const data = rcfile('mamock');
    

    bin命令的使用

    我们在package.json的scripts编写了mamock --port 3001命令,是怎么实现的呢。

    在package.json添加

    "bin": {
        "ma-mock": "./bin/mock-proxy.js",
        "mamock": "./bin/mock-proxy.js"
    }
    

    在bin目录添加mock-proxy.js

    #!/usr/bin/env node
    
    'use strict';
    
    // 定义用到的参数
    const keys = ['port', 'prefix', 'rootPath', 'proxyPath', 'proxyFilename'];
    const argvs = process.argv.slice(2);
    function getArgv(key) {
      const index = argvs.findIndex(it => it === `--${key}`);
      return index >= 0 && argvs[index + 1];
    }
    keys.forEach(key => {
      const value = getArgv(key);
      if (value) process.env[key] = value;
    });
    // 执行koa的index.js
    require('../server/index.js');
    
    

    自动打开浏览器

    启动ma-mock服务时自动打开浏览器

    // index.js
    const opn = require('opn');
    
    app.listen(PORT);
    
    opn(`http://localhost:${PORT}`, {app: 'google chrome'});
    

    总结

    目前还是比较粗糙,后端只做了简单的joi参数校验,没对mock地址进行url校验;mock数据也只支持基本的application/json类型,且不支持mock.js的配置;最重要的,单元测试没补全,后续会慢慢填。

    如果有什么建议或者想贡献代码,可以发issue和pr,项目地址: https://github.com/masongzhi/ma-mock

    相关文章

      网友评论

        本文标题:手动编写mock服务(ma-mock)

        本文链接:https://www.haomeiwen.com/subject/bdslmftx.html