前端工程化之数据mock

1,789 阅读5分钟

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>

得到效果类似于这样:

image.png

但是这样做,不灵活,也不优雅,后期接口调完了,还需要将这些假数据一一删除,显得比较麻烦,有没有更好的方式呢,于是又想到了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
}

得到的效果如下图所示:

image.png

这样看,确实要优雅一点,但是少了点真实性,就是没有走网络,感觉不是一接口,就是一个方法。于是我们通过引入easymock、postman等工具,利用它们的mock服务来实现数据mock。

image.png

这里我们再次对代码进行改造:

// 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')
}

得到效果如图所示:

image.png 如我们所想,完美地发送了接口,并得到正确的结果,但是这样做,也存在些不足,那就是我整个应用都需要走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

得到效果如图:

image.png 也能得到想要的效果。还以对指定接口做数据mock。

只是这样做,需要了解一些服务端的知识,同时还要启两个服务,还是显得有些麻烦,可不可以就启一个服务,就能实现上面的效果呢?

思来想去,webpack-dev-server本身不也是个web服务器吗?可不可以在它身上找文章,这一找,还真找出点东西。

image.png 可以利用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,可看到如下效果:

image.png

这个虽然也很不错,但是也有不足,比如服务端修改了代码,不能进行热更替。那有什么办法呢?

这里我想了两种方法。

方法1: 将数据单独提取出来,存放在一个文件中,每次调用接口,都通过读取文件的方式来读取数据。这样可以实现数据的实时更新。但如果对接口有逻辑变动,还是需要重启服务来解决。

方法2: 利用第三方工具Apifox, 它能提供智能mock,使用起来,更加简单。

image.png 只需要将原来通过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

最后得到效果如下:

image.png

写到最后,我也对上述内容,做一个简单的总结,个人认为是目前最好用的mock方式。 就是用自写服务 + apifox相结合的方式,这既可以实现服务端的自动重启,同时,也可以实现数据的智能mock。