1、什么是Mock?
如果将mock单独翻译过来,其意义为“虚假”,在我们软件开发领域,我们可以理解为通过一些技术手段,来制造一些“假数据”,来模拟一些真实场景。
2、为什么要用Mock?
为什么要用Mock,我平时不用Mock,也可以跟后端做接口联调,一样,可以开发地好好的?
基于上面的问话,我内心其实也是认同的,但这有个前提,就是你跟你的后端同学非常有默契,能够达到统一的认知,在无需太多的言语,就能心领神会。
但是大多时候,我们是达不到上面所说的程度,而且现在前后端并行开发也是主流,前端开发界面,后端开发接口,可大家经常忽略一件事情,是什么呢?就是接口联调。
在现实情况里,很多的同学对自己任务估时不准确,给到联调的时间不充分,导致有时候联调一个接口,花费大量的时间,而导致任务延期,或者任务质量无法保证。那是什么原因导致的?
- 1、 接口字段定义模糊不清,字段描述用后端术语或者数据库术语,而非业务术语。
- 2、前端值传错,后台接口,没有明确的错误提示,往往报一个含糊的提示语,如“业务异常”,或者“服务器错误”等,需要等后端去看日志排查,再反馈到前端。
- 3、前端值传错了,可后端没有报错,数据成功入库了,导致后续流程出错。
- 4、...
3、如何避免这种情况?
从前端的角度出发,就是提前发现错误,并解决错误,而不是到联调时,再统一去发现,并解决错误。也就是说,我们要在联调之前,加一层漏斗,让我们的错误减少,提高联调质量,那要怎么做呢?
没错呢,就是使用mock,我们通过遵守接口协议,保证传递给后端的数据是完全遵守接口协议,且准确无误的,那我们是不是就更有底气。这时也许有同学会说,后端并没有遵守接口协议,悄悄改了字段,怎么办,说到这里,其实,我也没有好办法,我想可能只有通过制度去规范,如果接口有变动,要第一时间通知到相关人员。
4、那要怎么去做Mock?
最开始,做数据Mock还是比较简单粗暴的,就是直接在文件中将数据写死,类似于下图这个样子。
<template>
<div class="hello">
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="日期" width="180"> </el-table-column>
<el-table-column prop="name" label="姓名" width="180"> </el-table-column>
<el-table-column prop="address" label="地址"> </el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {
tableData: [
{
date: "2016-05-02",
name: "王小虎",
address: "上海市普陀区金沙江路 1518 弄",
},
{
date: "2016-05-04",
name: "王小虎",
address: "上海市普陀区金沙江路 1517 弄",
},
{
date: "2016-05-01",
name: "王小虎",
address: "上海市普陀区金沙江路 1519 弄",
},
{
date: "2016-05-03",
name: "王小虎",
address: "上海市普陀区金沙江路 1516 弄",
},
],
};
}
};
</script>
得到效果类似于这样:
但是这样做,不灵活,也不优雅,后期接口调完了,还需要将这些假数据一一删除,显得比较麻烦,有没有更好的方式呢,于是又想到了mockjs,于是我将上面的代码进行了一点点改造。
<template>
<div class="hello">
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="日期" width="180"> </el-table-column>
<el-table-column prop="name" label="姓名" width="180"> </el-table-column>
<el-table-column prop="address" label="地址"> </el-table-column>
</el-table>
</div>
</template>
<script>
import {getUserList} from '@/api/user'
export default {
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {
tableData: [],
};
},
async mounted() {
const res = await getUserList()
this.tableData = res.data
},
};
</script>
// api/user.js
import Mock from 'mockjs'
export async function getUserList() {
const data = Mock.mock({
'data|1-10': [
{
date: "@date",
name: "@cname",
address: "@county(true)",
}
]
})
return data
}
得到的效果如下图所示:
这样看,确实要优雅一点,但是少了点真实性,就是没有走网络,感觉不是一接口,就是一个方法。于是我们通过引入easymock、postman等工具,利用它们的mock服务来实现数据mock。
这里我们再次对代码进行改造:
// utils/request.js
import axios from 'axios'
const instance = axios.create({
// 将easymock的baseUrl写在这里
baseURL: 'http://easymock.dev.newhopescm.com/mock/6201d346b06fb30021796a76/example',
timeout: '5000'
})
// 请求拦截
instance.interceptors.request.use(config => {
return config
}, error => {
Promise.reject(error)
})
// 响应拦截
instance.interceptors.response.use(response => {
const {status, data} = response
if (status === 200) {
return data
}
}, error => {
Promise.reject(error)
})
const request = Object.create(instance)
request.get = function(url, data={}, config={}) {
return instance.get(url, {...data, ...config})
}
request.post = function(url, data={}, config={}) {
return instance.post(url, data, config)
}
request.put = function(url, data={}, config={}) {
return instance.put(url, data, config)
}
request.delete = function(url, data={}, config={}) {
return instance.delete(url, {...data, ...config})
}
export default request
// api/user.js
import request from '@/utils/request'
export function getUserList() {
return request.get('/getUserList')
}
得到效果如图所示:
如我们所想,完美地发送了接口,并得到正确的结果,但是这样做,也存在些不足,那就是我整个应用都需要走mock,否则应用就跑不起来,这样一想,,在小型项目中还可行,但如果在大型应用中,就行不通了,于是,我又进行了尝试,就想着在项目启动时,再启一个mock服务,框架嘛,就使用express或者koa,说干就干,于是又对原有项目进行了改造,新增了一个server文件夹。
// app.js
const Koa = require('koa')
const requireDirectory = require('require-directory')
const Router = require('koa-router')
// 创建一个实例
const app = new Koa()
// 挂载路由
requireDirectory(module, './router', {
visit(router) {
if (router instanceof Router) {
app.use(router.routes())
app.use(router.allowedMethods())
}
}
})
app.listen(3000, () => {
console.log('服务运行在3000端口上...')
})
在新增一个路由文件
// router/user.js
const Router = require('koa-router')
const Mock = require('mockjs')
const router = new Router({
// prefix: '/'
})
router.get('/getUserList', async ctx => {
const data = Mock.mock({
'data|1-10': [
{
date: "@date",
name: "@cname",
address: "@county(true)"
}
]
})
ctx.body = data
})
module.exports = router
在vue.config.js中添加代理
// 默认的devServer配置
const defaultDevServer = {
proxy: {
'/api': {
target: 'http://localhost:3000',
ws: false,
changeOrigin: true,
pathRewrite: {'^/api': '/'}
}
}
}
module.exports = {
devServer: defaultDevServer,
productionSourceMap: false,
configureWebpack: {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
}
}
在package.json中配置scripts:
"scripts": {
"mock": "nodemon server/app.js",
"serve": "cross-env VUE_APP_ENV=dev vue-cli-service serve"
},
为了可以一行命令启动两个服务,可以通过concurrently这个模块来实现, 同时为了保证服务器文件改动自动重启,引入了nodemon模块。
yarn add concurrently nodemon@1.19.0
在scripts中再添加一个start命令:
"scripts": {
"mock": "nodemon server/app.js",
"serve": "cross-env VUE_APP_ENV=dev vue-cli-service serve",
"start": "concurrently \"npm run mock\" \"npm run serve\""
},
通过npm start来启动。
npm start
得到效果如图:
也能得到想要的效果。还以对指定接口做数据mock。
只是这样做,需要了解一些服务端的知识,同时还要启两个服务,还是显得有些麻烦,可不可以就启一个服务,就能实现上面的效果呢?
思来想去,webpack-dev-server本身不也是个web服务器吗?可不可以在它身上找文章,这一找,还真找出点东西。
可以利用
before函数,挂载我们的路由中间件。
// mock/server.js
const Mock = require('./index')
const requireDirectory = require('require-directory')
const isMock = process.env.VUE_APP_ENV === 'mock'
// 代理对象
const proxy = {}
requireDirectory(module, `${process.cwd()}/mock/api`, {
visit(router) {
const stacks = router.stack
for (let stack of stacks) {
const route = stack.route
proxy[route.path] = {
target: 'http://localhost:8089',
ws: false,
changeOrigin: true,
pathRewrite: {'^/api': '/'}
}
}
}
})
const defaultServer = {
port: 8089,
disableHostCheck: true,
hot: true,
compress: true,
before(app) {
if (isMock) {
Mock(app)
}
},
proxy: {
...proxy,
'/api': {
target: 'http://localhost:8089',
ws: false,
changeOrigin: true,
pathRewrite: {'^/api': '/'}
}
}
}
module.exports = defaultServer
路由中间件函数
// mock/index.js
const requireDirectory = require('require-directory')
module.exports = (app) => {
requireDirectory(module, `${process.cwd()}/mock/api`, {
visit(router) {
app.use(router.prefix || '/', router)
}
})
}
// mock/api/user.js
const express = require('express')
const Mock = require('mockjs')
const router = express.Router()
router.get('/getUserList', async(req, res) => {
const data = Mock.mock({
'data|1-10': [
{
date: "@date",
name: "@cname",
address: "@county(true)"
}
]
})
res.json(data)
})
module.exports = router
改造vue.config.js配置:
// vue.config.js
// 获取运行环境
const env = process.env.VUE_APP_ENV
// 判断是否是mock环境
const isMock = env === 'mock'
// 判断是否是开发环境
// const isDev = env === 'dev'
// 默认的devServer配置
const defaultDevServer = {
proxy: {
'/api': {
target: 'http://localhost:3000',
ws: false,
changeOrigin: true,
pathRewrite: {'^/api': '/'}
}
}
}
module.exports = {
devServer: isMock ? require('./mock/server') : defaultDevServer,
productionSourceMap: false,
configureWebpack: {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
}
}
启动服务npm run mock,可看到如下效果:
这个虽然也很不错,但是也有不足,比如服务端修改了代码,不能进行热更替。那有什么办法呢?
这里我想了两种方法。
方法1: 将数据单独提取出来,存放在一个文件中,每次调用接口,都通过读取文件的方式来读取数据。这样可以实现数据的实时更新。但如果对接口有逻辑变动,还是需要重启服务来解决。
方法2: 利用第三方工具Apifox, 它能提供智能mock,使用起来,更加简单。
只需要将原来通过mock数据的地方更换为使用接口的方式去调用:
const { default: axios } = require('axios')
const express = require('express')
const router = express.Router()
router.get('/getUserList', async(req, res) => {
const data = await axios.get('http://127.0.0.1:4523/mock/616873/getUserList')
res.json(data.data)
})
module.exports = router
最后得到效果如下:
写到最后,我也对上述内容,做一个简单的总结,个人认为是目前最好用的mock方式。 就是用自写服务 + apifox相结合的方式,这既可以实现服务端的自动重启,同时,也可以实现数据的智能mock。