前言
本文主要面向前端开发工程师,讲解如何使用Node.js框架Koa2和mockjs搭建一套数据模拟服务,为前端服务提供模拟数据,简易操作即可极大提高开发效率。
实战中遇到的问题
- 场景一: 小A同学正在快乐的写着CSS,突然接口全部404了,一问小B后端同学,发现是小C同学偷偷把后端分支C的代码部署到开发环境。原来开发环境的分支A代码被覆盖了。后端分支A(小B)的代码才能提供给小A同学对接接口。
- 场景二: 小A同学根据小B同学返回的数据快乐的写着JS逻辑,突然接口报500并告知内部异常,返回给后端小B同学,小B同学淡定的回了一句:"我看看"。刚刚的逻辑思维突然被这个500打断了。
- 场景三: 小A同学拿到一个列表渲染接口,返回一切正常,只是"list:[]"。于是小A同学让小B同学在接口加点数据,小B看到每条记录要加30几个数据,还有10多个接口和10多个bug没有改,
最主要的是还有5分钟就到6点了。所以小B回复了一个表情包
上述场景,都能通过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的弊端还是有的,我们来总结一下。
- 需要等待后端同学提供返回数据字段和类型
- 需要对nodejs有一定的了解,不过我相信大家都是热爱学习的三好学生。
总结
前端工程师自己搭建一套mockserver,定义好返回数据结构后,可以和后端并行开发,且可以自行模拟全套流程,在流程中能够提前发现问题并解决问题,能够更快的完成前后端联调,加快提测上线。