使用Koa2打造mockserver

前言

本文主要面向前端开发工程师,讲解如何使用Node.js框架Koa2和mockjs搭建一套数据模拟服务,为前端服务提供模拟数据,简易操作即可极大提高开发效率。

实战中遇到的问题

  1. 场景一: 小A同学正在快乐的写着CSS,突然接口全部404了,一问小B后端同学,发现是小C同学偷偷把后端分支C的代码部署到开发环境。原来开发环境的分支A代码被覆盖了。后端分支A(小B)的代码才能提供给小A同学对接接口。
  2. 场景二: 小A同学根据小B同学返回的数据快乐的写着JS逻辑,突然接口报500并告知内部异常,返回给后端小B同学,小B同学淡定的回了一句:"我看看"。刚刚的逻辑思维突然被这个500打断了。
  3. 场景三: 小A同学拿到一个列表渲染接口,返回一切正常,只是"list:[]"。于是小A同学让小B同学在接口加点数据,小B看到每条记录要加30几个数据,还有10多个接口和10多个bug没有改,最主要的是还有5分钟就到6点了。所以小B回复了一个表情包


使用Koa2打造mockserver_第1张图片

上述场景,都能通过mockserver得以解决。那我们直接进入正题。

Server介绍

Server采用的是Koa2进行搭建,先看一波项目结构

|-- app
    |-- controller        // 控制器
    |-- route             // 路由
    |-- validation        // 利用joi进行参数验证
|-- config
    |-- config.js         // 项目配置
|-- logs                  // 日志
|-- middleware            // 中间件
|-- util                  // 公共工具库
|-- app.js                // 项目启动文件
|-- package.json          // node项目文件描述

然后我们挑几个重点讲一下

const Koa = require('koa');
const app = new Koa();
const config = require('./config/config.js');
var cors = require('koa-cors');
app.use(cors());


/**
 * 添加报文解析中间件
 */
const bodyParser = require('koa-bodyparser');
const xmlParser = require('koa-xml-body');
app.use(xmlParser());
app.use(bodyParser());

/**
 * 处理输入报文中间件
 */
app.use(require('./middleware/input.js'));

/**
 * 处理header中间件
 */
app.use(require('./middleware/header.js'));

/**
 * 请求回调处理中间件
 */
app.use(require('./middleware/requestError.js'));

/**
 * 加载路由
 * 路由配置在config/config.js中 routes 数组中
 */
let routes = config.routes;
for (let key in routes) {
    if (routes.hasOwnProperty(key)) {
        let element = routes[key];
        let router = require(element)();
        app.use(router.routes());
    }
}


app.listen(config.port);
module.exports = app;

Koa2自我介绍阐述它是一个洋葱模型,用context包装Request请求参数,通过一层层中间件处理数据,然后再返回response数据。下面我介绍一下这套框架的重点。

路由

路由使用了koa-router这个中间件,在route文件夹中新建一个example文件夹,对应前端的一个项目。然后在config.js加上配置。这样做为了多个项目的时候可以以文件夹的形式进行路由管理。

// config/config.js
/**
 * 应用要加载的路由配置
 */
"routes": [
    './app/route/example/index.js'
]

// app.js
/**
 * 加载路由
 * 路由配置在config/config.js中 routes 数组中
 */
let routes = config.routes;
for (let key in routes) {
    if (routes.hasOwnProperty(key)) {
        let element = routes[key];
        let router = require(element)();
        app.use(router.routes());
    }
}

当我们需要加一个对应mock接口,如下代码

// app/route/example/index.js
const Router = require('koa-router');
const activityController = require('../../controller/example/activity.js');

module.exports = function() {
    let router = new Router();

    // 获取记录列表
    router.get('/activity-parcel-service/parcel/lottery/winners/red', activityController.getResultList);
}

router的方法遵循restful api, 主要有以下几种
get, post, put, delete
router 接受两个参数: path(url), 还有多个中间件

activityController为控制器,getDefaultActivity是这个控制器里面的一个方法,而且也是一个中间件,控制器下文会说到

控制器

我在controller文件夹新建了一个example项目,在这个项目新建了一个名为activity.js的文件,在这个文件写了名为getResultList的方法。

const response = require('../../../util/response.js');
const validator = require('../../../util/requestValidator.js');
const activityValidation = require('../../validation/example/activity.js');
const Mock = require('mockjs');

module.exports = {
    /**
    * @methods 获取结果页
    * @param {Object} ctx koa上下文 context
    **/
    getResultList: async function(ctx, next) {
        // 判断接口是否有传id
        await validator.validate(
            ctx.input,
            activityValidation.getResultList.schema,
            activityValidation.getResultList.options
        )
        const Random = Mock.Random;
        Random.word()
        Random.name()
        let data = Mock.mock({
            'describeList|1-5': ['@word'],
            'parcelRecordRedWinnerVoList|1-10': [{
                'avatarUrl': Random.image('100x100'), // 如果要给image指定大小
                'bonusAmount|1-2.1-2': 1,
                'nickname': '@name'
            }]
        })
        return response.map(ctx, data);
    }
}

通过代码可以看到,我们将mockjs引用到了controller层。访问MockServer接口后,先通过路由转到controller层,再处理逻辑返回信息。

mockjs的用法下文会有解释。

validator的主要用途在于,可以校验前端请求带来的参数,因为这个接口本身是需要传一个参数"id"的,然后这里通过validator做了一层处理,如果id不合法,就会抛出异常,提示如下信息。

全局异常捕获

上面之所以能弹出参数错误,是因为有一个全局异常捕捉的中间件

/**
 * 请求回调处理中间件
 */
app.use(require('./middleware/requestError.js'));
const response = require('../util/response.js');

module.exports = async function (ctx, next) {
  try {
    await next();
  } catch(e) {
    ctx.status = status;
    let errCode = e.code || 1;
    let msg = e.message || e;
    if (!(parseInt(errCode) > 0)) {
        errCode = 1;
    }
    return response.output(ctx, {}, errCode, msg, status);
  }
}

当我们抛出异常的时候,就会被这个中间件捕捉并处理,返回我们需要抛出的错误信息给到前端。

Mockjs

官网地址: http://mockjs.com

优势

直接截取官网配图

用法

因为偷懒,这里我只说几个日常用的,具体的请看官网

字符串

'name|min-max': string
Mock.mock({
  "string|1-10": "str"
})
// "str"会被复制1-10个
{
  string: "strstrstr"
}

数字

'name|min-max': number
Mock.mock({
  "number|1-10": 1
})
// 返回对象1-10的随机数
{
  number: 9
}

图片

Random.image( size?, background?, foreground?, format?, text? )
size: 图片大小,如"100x100"
background: 背景颜色
foreground: 字体颜色
format: 格式  png|jpg
text: 文字

对象

'name|min-max': object
Mock.mock({
  "data|2-4": {
    "id": 1,
    "id": 2,
    "id": 3,
    "id": 4
  }
})
// 返回对象里面2-4个已经定义好的元素
{
  id: 1,
  id: 2
}

数组

'name|min-max': array
Mock.mock({
  "array|1-4": [
    {
      "name|+1": [
        "Hello",
        "Mock.js",
        "!"
      ]
    }
  ]
})
// 返回数组里面定义好的1-4个元素
{
  "array": [
    {
      "name": "Hello"
    },
    {
      "name": "Mock.js"
    }
  ]
}

上面简单介绍了mockjs常用的几个类型,接下来讲解如何使用mockserver。

项目中如何使用

mockserver启动

第一步: 下载项目mockserver

https://github.com/FEA-Dven/m...

第二步: 全局安装pm2,pm2主要用来管理node的进程

cnpm install pm2 -g or npm install pm2 -g

第三步:

cd mockserver & npm install

第四步:
在路由文件添加URL,URL就是实际项目中使用到的URL,这里我使用了红包项目的URL

const Router = require('koa-router');
const activityController = require('../controller/example/activity.js');

module.exports = function() {
    let router = new Router();
    // 获取默认活动配置
    router.get('/activity-parcel-service/parcel/lottery/getDefaultActivityId', activityController.getDefaultActivity);
}

第五步:
在controller添加一个文件,每个文件对应前端项目,这里我使用了example代替,我在controller新建了一个example文件夹,添加了一个activity.js的文件

const response = require('../../../util/response.js');
const Mock = require('mockjs');

module.exports = {
    /**
    * @methods 获取默认活动
    */
    getDefaultActivity: async function(ctx, next) {
        // 可以校验前端传递的参数
        Mock.Random.id();
        let data = Mock.mock({
            'dataStatus|0-1': 0,
            'id': '@id',
            'styleType': 1
        })
        return response.map(ctx, data);
    }
}

第六步:
返回到根目录,用pm2开启监听模式

pm2 start app.js --watch

pm2用法

  • 打开pm2进程列表
pm2 list
  • 查看服务日志
pm2 logs 0 // 0代表进程ID

使用postman请求我们的URL就能返回MOCK数据了

现在服务是搭建完成了,还需要跟前端服务对接起来

应用到日常开发

先看看我们原来的封装请求函数

// utils.request.js
/**
 * @function 微信请求方法封装
 * @param {string} method 请求类型
 * @param {string} host 域名地址
 * @param {string||array} url 接口地址
 * @param {object||array} data 参数数据
 * @param {boolean} showModal 是否显示错误弹窗
 * @return {object} 请求回来的数据
 */
export const requestFn = ({
  method = 'GET',
  host = wx.$CONFIG.API_URL,
  url = '',
  data = {},
  token = true,
  header = {
    'content-type': 'application/json'
  },
  showLoading = false, // 是否菊花
  showModal = false // 是否弹窗
}) => {
  return new Promise((resolve, reject) => {
    // 展示loading动画
    if (showLoading) wx.showLoading();
    // token校验
    if (token) {
      let _token = wx.$USER.getToken();
      if (_token) {
        header[wx.$CONFIG.TOKEN_CACHE] = _token
        
      } else {
        return;
      }
    }

    // 开始请求
    wx.request({
      method: method.toUpperCase(),
      url: host + url,  // 全局变量host
      header,
      data,
      success: res=> {
        resolve(res.data);
      },
      fail: err => {}
    })
  })
}

我们看到里面有一个变量host,只要更改这个变量host指向我们本地的mock域名,就能收工。

当然是骗你们的啦

一般中小型的项目,会有几十个接口,大型的成千个接口,如果host直接更改为我们的Mock域名,我们的mockserver路由岂不是也要写上成千上百个路由接口?这是反人类的操作。所以我们需要进行优化更改

前端请求封装更改

请求服务接口的时候,我们都会有一个api文件,先看下原来的代码

/**
 * 获取默认活动id
 */
function getDefaultActivityId (data = {}) {
  return requestFn ({
    url: '/activity-parcel-service/parcel/lottery/getDefaultActivityId',
    method: 'get',
    data,
    showLoading: false,
    showModal: false
  })
}

这里我们需要加一个参数,告诉封装请求文件这个api需要进行mock数据,所以我们加了一个"isMock"变量

/**
 * 获取默认活动id
 */
function getDefaultActivityId (data = {}, isMock) {
  return requestFn ({
    url: '/activity-parcel-service/parcel/lottery/getDefaultActivityId',
    method: 'get',
    data,
    showLoading: false,
    showModal: false,
    isMock
  })
}

微信请求方法封装文件也对应进行更改

// utils.request.js
// 头部定义MOCK_HOST
const MOCK_HOST = 'http://10.154.68.180:8080';
/**
 * @function 微信请求方法封装
 * @param {string} method 请求类型
 * @param {string} host 域名地址
 * @param {string||array} url 接口地址
 * @param {object||array} data 参数数据
 * @param {boolean} showModal 是否显示错误弹窗
 * @return {object} 请求回来的数据
 */
export const requestFn = ({
  method = 'GET',
  host = wx.$CONFIG.API_URL,
  url = '',
  data = {},
  token = true,
  header = {
    'content-type': 'application/json'
  },
  showLoading = false, // 是否菊花
  showModal = false, // 是否弹窗
  isMock
}) => {
  return new Promise((resolve, reject) => {
    // 展示loading动画
    if (showLoading) wx.showLoading();
    // token校验
    if (token) {
      let _token = wx.$USER.getToken();
      if (_token) {
        header[wx.$CONFIG.TOKEN_CACHE] = _token
        
      } else {
        return;
      }
    }
    // 判断是否需要开启模拟参数形式返回数据
    host = `${isMock ? MOCK_HOST : host}`;
    // 开始请求
    wx.request({
      method: method.toUpperCase(),
      url: host + url,
      header,
      data,
      success: res=> {
        resolve(res.data);
      },
      fail: err => {}
    })
  })
}

调用API的时候传上isMock为true参数

wx.$API.getDefaultActivityId({ ...otherParams, isMock: true })

看下效果:

其他方案对比

YApi

YApi作为强大的API文档管理工具,自然也可以使用mock数据。如果要使用YApi进行MOCK数据,最简单的方法是内网部署

地址如下

https://hellosean1025.github....

在我看来,如果仅需要使用mock,YApi过于庞大繁琐,且需要依赖mongodb环境,还需要账号登录等。而如果使用自己搭建的mockserver,只需要nodejs安装依赖就能运行。

其他同学有更好的方案欢迎交流

弊端

说了这么多,mockserver的弊端还是有的,我们来总结一下。

  1. 需要等待后端同学提供返回数据字段和类型
  2. 需要对nodejs有一定的了解,不过我相信大家都是热爱学习的三好学生。

总结

前端工程师自己搭建一套mockserver,定义好返回数据结构后,可以和后端并行开发,且可以自行模拟全套流程,在流程中能够提前发现问题并解决问题,能够更快的完成前后端联调,加快提测上线。

你可能感兴趣的:(javascript,mock.js,koa2)