前言

最近一直在捣鼓毕设,准备做的是一个基于前后端开发的Mock平台,前期花了很多时间完成了功能模块的交互。现在进度推到如何设计核心功能,也就是Mock数据的解析。

根据之前的需求设定加上一些思考,用户可以像写json一般轻松完成数据的mock,也可以通过在mock数据模型之上进行构建出复杂的数据模型并在项目中引用。

这看似简单的需求其实需要处理几个不同的模块功能以及交互设计。该如何处理解析不同mock数据并进行构造?前端交互中模拟数据该如何处理?数据构造时如何加载用户设定的数据模型?错误捕捉与处理?

这些都暂时没有一个好的处理结果。因此想要完成核心功能我们需要明确需求,并且通过同类产品是如何处理的,通过阅读它们的源码来学习思想并加入。

明确需求

在明确该功能模块之前我们可以通过模拟流程来明确。

用户 -> 添加数据模型 - > 实时看到构造结构

用户 -> 添加接口 -> 构造json格式返回参数 -> 预览

构造json格式返回参数 不仅包含返回的正文,同时也设定了 header 和 method。

阅读源码

符合大部分需求的开源项目有

  1. mock.js
  2. easy-mock
  3. eolinker
  4. YAPI
  5. DOCCLEVER

MOCK.JS篇

首先我们需要明确现阶段大部门的 Mock 平台或多或少都是受到 Mock.js 的思想或者是其增强版。

我们可以用下面简单的 json 通过 Mock.js来构造数据:


  1. example:
  2. {
  3. "status|0-1": 0, //接口状态
  4. "message": "成功", //消息提示
  5. "data": {
  6. "counts":"@integer", //统计数量
  7. "totalSubjectType|1-4": [ //4-10意味着可以随机生成4-10组数据
  8. {
  9. "subjectName|regexp": "大数据|机器学习|工具", //主题名
  10. "subjectType|+1": 1 //类型
  11. }
  12. ],
  13. "data":[
  14. {
  15. "name": "@name", //用户名
  16. "cname":"@cname",
  17. "email": "@email", //email
  18. "time": "@datetime" //时间
  19. }
  20. ]}
  21. }

返回结果


  1. {
  2. "status": 0,
  3. "message": "成功",
  4. "data": {
  5. "counts": 2216619884890228,
  6. "totalSubjectType": [
  7. {
  8. "subjectNameregexp": "大数据|机器学习|工具",
  9. "subjectType": 1
  10. },
  11. {
  12. "subjectNameregexp": "大数据|机器学习|工具",
  13. "subjectType": 2
  14. },
  15. {
  16. "subjectNameregexp": "大数据|机器学习|工具",
  17. "subjectType": 3
  18. },
  19. {
  20. "subjectNameregexp": "大数据|机器学习|工具",
  21. "subjectType": 4
  22. }
  23. ],
  24. "data": [
  25. {
  26. "name": "Ruth Thompson",
  27. "cname": "鲁克",
  28. "email": "z.white@young.gov",
  29. "time": "1985-02-06 05:45:21"
  30. }
  31. ]
  32. }
  33. }

而且可以通过其 Mock.Random.extend() 来扩展自定义占位符.


  1. example:
  2. Random.extend({
  3. weekday: function(date) {
  4. var weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  5. return this.pick(weekdays);
  6. },
  7. sex: function(date) {
  8. var sexes = ['男', '女', '中性', '未知'];
  9. return this.pick(sexes);
  10. }
  11. });
  12. console.log(Random.weekday()); // 结果: Saturday
  13. console.log(Mock.mock('@weekday')); // 结果: Tuesday
  14. console.log(Random.sex()); // 结果: 男
  15. console.log(Mock.mock('@sex')); // 结果: 未知

来延伸所需进的拓展。

这个可以将自定义数据模型先进行解析,然后通过extend将其加入。

easy-mock

easy-mock 是我参考的主要项目之一,它的UI交互非常符合我的设定,而且作为开源项目可以从它的源码中学到很多。

直接来看它提供接口编辑的页面


  1. {
  2. data: {
  3. img: function({
  4. _req,
  5. Mock
  6. }) {
  7. return _req.body.fileName + '_' + Mock.mock('@image')
  8. }
  9. }
  10. }

可以从上得之它既可以处理Mock数据模拟也可以处理函数,而且它内部有一套能处理req的内容。

先是在源码中找了一下,找到几个疑似点,但是不确定,还是在本地装好环境,主要是需要按照redis.然后启动服务去打几个断点输出。

根据经验先确定 controllers\mock.js 应该是处理数据模拟的地方。通过浏览源码并分析,最终定位于 297行处的代码


  1. await redis.lpush('mock.count', api._id)
  2. if (jsonpCallback) {
  3. ctx.type = 'text/javascript'
  4. ctx.body = `${jsonpCallback}(${JSON.stringify(apiData, null, 2)})`
  5. .replace(/\u2028/g, '\\u2028')
  6. .replace(/\u2029/g, '\\u2029') // JSON parse vs eval fix. https://github.com/rack/rack-contrib/pull/37
  7. } else {
  8. ctx.body = apiData
  9. }

首先是看到最终返回的 apiData 。用过 koa 或者 express 都应该清楚 ctx.body 的含义。然后我在上面写了句 console.log(apiData)

然后在浏览器端发送请求。看下 node 端输出和浏览器端拿到的数据,基本可以肯定最终输出就是这个。

然后我们往上翻,可以看到这么一段代码:


  1. const vm = new VM({
  2. timeout: 1000,
  3. sandbox: {
  4. Mock: Mock,
  5. mode: api.mode,
  6. template: new Function(`return ${api.mode}`) // eslint-disable-line
  7. }
  8. })
  9. console.log('数据验证')
  10. console.log(mode)
  11. vm.run('Mock.mock(new Function("return " + mode)())') // 数据验证,检测 setTimeout 等方法
  12. apiData = vm.run('Mock.mock(template())') // 解决正则表达式失效的问题

通过查询了解到 VM 是一个沙盒,可以运行不受信任的代码。

大概就能了解 easy-mock 通过 vm 沙盒模式运行 mode 代码解析后返回结果。

核心代码就是 Mock.mock( template ) 这么一句。根据数据模板生成模拟数据。

通过查文档了解 template 是可以直接内部写函数然后执行的。

这样解析的难度大大下降,发现原来并没有特别复杂的,依旧是依赖了 Mock.js 的原生方法。

然后我们可以看到 easy-mock 另一的操作就是可以获取 请求参数_req。也就是可以通过以下代码来根据请求参数返回指定数据。


  1. {
  2. success: true,
  3. data: {
  4. default: "hah",
  5. _req: function({
  6. _req
  7. }) {
  8. return _req
  9. },
  10. name: function({
  11. _req
  12. }) {
  13. return _req.query.name || this.default
  14. }
  15. }
  16. }

_req 一看就是从请求参数中获得的对象。

Mock.js是没有这个对象的,我们来找找源码中是哪里注入了这个对象。

还是在 mock.js 这个文件中第234行处找到


  1. Mock.Handler.function = function (options) {
  2. const mockUrl = api.url.replace(/{/g, ':').replace(/}/g, '') // /api/{user}/{id} => /api/:user/:id
  3. options.Mock = Mock
  4. options._req = ctx.request
  5. options._req.params = util.params(mockUrl, mockURL)
  6. options._req.cookies = ctx.cookies.get.bind(ctx)
  7. return options.template.call(options.context.currentContext, options)
  8. }

通过阅读 MockJS 的源码,了解到 Handler是处理数据模板的地方,打个断点再输出一次可以发现其实是在 Mock.mock(new Function("return " + mode)())' 之后传入的参数。

options._req = ctx.request 这句代码告诉了我们所谓的 _req是从哪里来的。

因此这个技术点我们也了解了是怎么做的,那么剩下一个灵活的支持 restful 通过阅读源码发现其实也没怎么处理,只是用 pathToRegexp 进行了一次验证。它先是在 middlewares/index.js 中 的 mockFilter 进行了路径正则。


  1. static mockFilter (ctx, next) {
  2. console.log(ctx.path)
  3. const pathNode = pathToRegexp('/mock/:projectId(.{24})/:mockURL*').exec(ctx.path)
  4. console.log(pathNode)
  5. if (!pathNode) ctx.throw(404)
  6. if (blackProjects.indexOf(pathNode[1]) !== -1) {
  7. ctx.body = ctx.util.refail('接口请求频率太快,已被限制访问')
  8. return
  9. }
  10. console.log('通过筛选')
  11. ctx.pathNode = {
  12. projectId: pathNode[1],
  13. mockURL: '/' + (pathNode[2] || '')
  14. }
  15. return next()
  16. }

然后通过存在 redis 里的接口内容再进行了验证匹配。


  1. const { query, body } = ctx.request
  2. const method = ctx.method.toLowerCase()
  3. const jsonpCallback = query.jsonp_param_name && (query[query.jsonp_param_name] || 'callback')
  4. let { projectId, mockURL } = ctx.pathNode
  5. console.log('ctx.pathNode', ctx.pathNode)
  6. const redisKey = 'project:' + projectId
  7. let apiData, apis, api
  8. console.log('通过URL匹配检验')
  9. apis = await redis.get(redisKey)
  10. console.log(apis)
  11. if (apis) {
  12. apis = JSON.parse(apis)
  13. console.log('pure apis', apis)
  14. } else {
  15. apis = await MockProxy.find({ project: projectId })
  16. console.log('find projectId', apis)
  17. if (apis[0]) await redis.set(redisKey, JSON.stringify(apis), 'EX', 60 * 30)
  18. }
  19. if (apis[0] && apis[0].project.url !== '/') {
  20. mockURL = mockURL.replace(apis[0].project.url, '') || '/'
  21. }
  22. api = apis.filter((item) => {
  23. const url = item.url.replace(/{/g, ':').replace(/}/g, '') // /api/{user}/{id} => /api/:user/:id
  24. return item.method === method && pathToRegexp(url).test(mockURL)
  25. })[0]
  26. console.log('api',api)
  27. if (!api) ctx.throw(404)

基本不匹配的路径请求都是在 item.method === method && pathToRegexp(url).test(mockURL) 这句代码里被拦截的。

非常优秀的代码。通读下来,加上断点对其思路逻辑学到了很多。

eolinker

它的后端代码是 PHP 的,这就略过不看了。

YAPI

它的核心后端处理代码是在 mockServer.js

有了之前的阅读经验很快找到处理 Mock 数据的地方


  1. let res;
  2. res = interfaceData.res_body;
  3. try {
  4. if (interfaceData.res_body_type === 'json') {
  5. res = mockExtra(
  6. yapi.commons.json_parse(interfaceData.res_body),
  7. {
  8. query: ctx.request.query,
  9. body: ctx.request.body,
  10. params: Object.assign({}, ctx.request.query, ctx.request.body)
  11. }
  12. );
  13. try {
  14. res = Mock.mock(res);
  15. } catch (e) {
  16. yapi.commons.log(e, 'error')
  17. }
  18. }

非常简单粗暴的处理方法。。。

对增强功能比较好奇在, 于是在 common\mock-extra.js 里找到了 mock(mockJSON, context) 方法。根据参数其实就能了解绑定上下文然后做了一些动作。这里就不展开详细。等之后开发的时候用到再去细读。因为这是做了其自己的增强的Mock功能,而暂时不需要这方面的考虑。

DOClecer

这个项目是国内一个创业团队做的,我也加入了其官方群。虽然还没有用过。不过不妨碍阅读其源码了解思路。不过讲道理这个代码组织风格是挺糟糕的。。。

而且源码中不止一次出现了eval... 于是放弃参考。

写个小模块开心一下

通过阅读以上项目的源码,其实主要是前三个,感觉可以完成自己想要的需求了。那么先写一个小的来作为基础模块。


  1. export const mock = async(ctx: any) => {
  2. console.log('mock')
  3. console.log(ctx)
  4. console.log(ctx.params)
  5. const method = ctx.request.method.toLowerCase()
  6. // let { projectId, mockURL } = ctx.pathNode
  7. // 获取接口路径内容
  8. console.log('ctx.pathNode', ctx.pathNode)
  9. // 匹配内容是否一致
  10. console.log('验证内容中...')
  11. // 模拟数据
  12. Mock.Handler.function = function (options: any) {
  13. console.log('start Handle')
  14. options.Mock = Mock
  15. // 传入 request cookies,方便使用
  16. options._req = ctx.request
  17. return options.template.call(options.context.currentContext, options)
  18. }
  19. console.log('Mock.Handler', Mock.Handler.function)
  20. // const testMode = `{
  21. // 'title': 'Syntax Demo',
  22. // 'string1|1-10': '★',
  23. // 'string2|3': 'value',
  24. // 'number1|+1': 100,
  25. // 'number2|1-100': 100,
  26. // 'number3|1-100.1-10': 1,
  27. // 'number4|123.1-10': 1,
  28. // 'number5|123.3': 1,
  29. // 'number6|123.10': 1.123,
  30. // 'boolean1|1': true,
  31. // 'boolean2|1-2': true,
  32. // 'object1|2-4': {
  33. // '110000': '北京市',
  34. // '120000': '天津市',
  35. // '130000': '河北省',
  36. // '140000': '山西省'
  37. // },
  38. // 'object2|2': {
  39. // '310000': '上海市',
  40. // '320000': '江苏省',
  41. // '330000': '浙江省',
  42. // '340000': '安徽省'
  43. // },
  44. // 'array1|1': ['AMD', 'CMD', 'KMD', 'UMD'],
  45. // 'array2|1-10': ['Mock.js'],
  46. // 'array3|3': ['Mock.js'],
  47. // 'function': function() {
  48. // return this.title
  49. // }
  50. // }`
  51. const testMode = `{success :true, data: { default: "hah", _req: function({ _req }) { return _req }, name: function({ _req }) { return _req.query.name || this.default }}}`
  52. const vm = new VM({
  53. timeout: 1000,
  54. sandbox: {
  55. Mock: Mock,
  56. mode: testMode,
  57. template: new Function(`return ${testMode}`)
  58. }
  59. })
  60. vm.run('Mock.mock(new Function("return " + mode)())') // 数据验证,检测 setTimeout 等方法, 顺便将内部的函数执行了
  61. // console.log(Mock.Handler.function(new Function('return ' + testMode)()))
  62. const apiData = vm.run('Mock.mock(template())')
  63. console.log('apiData2333' , apiData)
  64. let result
  65. switch (method) {
  66. case 'get':
  67. result = success({'msg': '你调用了get方法'})
  68. break;
  69. case 'post':
  70. result = success({'msg': '你调用了post方法'})
  71. break;
  72. case 'put' :
  73. result = success({'msg': '你调用了put方法'})
  74. break;
  75. case 'patch' :
  76. result = success({'msg': '你调用了patch方法'})
  77. break;
  78. case 'delete' :
  79. result = success({'msg': '你调用了delete方法'})
  80. break;
  81. default:
  82. result = error()
  83. }
  84. // console.log(result)
  85. return ctx.body = result
  86. }

这里调试的遇到一些问题,主要是一开始测试的时候发现 Mock 只将规则的数据模拟出,发现 function 类型的函数都没执行,一开始定位以为是Mock.Handler.function 在 ts 中未执行。于是在里面写了一个输出,发现的确没有。经过各种猜想和测试,发现是模拟mode有问题。

一开始我是这么写的


  1. const testcode = {
  2. 'array1|1': ['AMD', 'CMD', 'KMD', 'UMD'],
  3. 'array2|1-10': ['Mock.js'],
  4. 'array3|3': ['Mock.js'],
  5. 'function': function() {
  6. return this.title
  7. }
  8. }

事实上应该这么写


  1. const testcode = `{
  2. 'array1|1': ['AMD', 'CMD', 'KMD', 'UMD'],
  3. 'array2|1-10': ['Mock.js'],
  4. 'array3|3': ['Mock.js'],
  5. 'function': function() {
  6. return this.title
  7. }
  8. }`

参照 easy-mock 的思路可以实现一个基础的 Mock数据解析器,而且可以根据 koa 的特性同时支持 _req 的一些参数,这里先不加进去。

如何支持自定义的数据模型也有了基本的思路,在之前没有考虑 redis 情况下还是用传统的数据库查询。具体实现等后期再捣鼓出来再写出来。

结尾

通过这两天的学习,总算把一个Mock的核心模块该如何实现的思路给理顺了。

其实无论你是用户自定义数据,比如


  1. {
  2. 'user': User, // User是用户自定义的数据类型
  3. 'string2|3': 'value',
  4. 'number1|+1': 100,
  5. _req: function({
  6. _req
  7. }) {
  8. return _req
  9. },
  10. name: function({
  11. _req
  12. }) {
  13. return _req.query.name || this.default
  14. }
  15. }

还是 Mock.js 原生的语法,你最终转换过来需要执行的是一样的内容,无非是在其转换前需要做一定的处理。只有搞懂了基本的数据模拟实现,基本上你可以将各个参数都做定制化。比如有的平台会将用户自己编写的函数一起和 json 拼接。其实用的最终核心思路还是一样的。

参考资料

Mock.js使用

mockjs官方文档

从零开始打造 Mock 平台 - 核心篇的更多相关文章

  1. 【Mock平台】测试开发实战01-开篇PRD和需求详细

    微信搜索[大奇测试开],关注这个坚持分享测试开发干货的家伙. 平台背景 从业务特性上,不少测试的服务很多是依赖第三方的接口的,比如其中的支付场景,就需要很多状态的返回进行验证,但大部分服务提供商没有很 ...

  2. 提高可测性-Mock平台设计和整体规划

    微信搜索[大奇测试开],关注这个坚持分享测试开发干货的家伙. 平台背景 从业务特性上,不少测试的服务很多是依赖第三方的接口的,比如其中的支付场景,就需要很多状态的返回进行验证,但大部分服务提供商没有很 ...

  3. 测试开发【Mock平台】04实战:前后端项目初始化与登录鉴权实现

    [Mock平台]为系列测试开发教程,从0到1编码带你一步步使用Spring Boot 和 Antd React 框架完成搭建一个测试工具平台,希望作为一个实战项目能为你的测试开发学习有帮助. 一.后端 ...

  4. [转] 前后端分离开发模式的 mock 平台预研

    引入 mock(模拟): 是在项目测试中,对项目外部或不容易获取的对象/接口,用一个虚拟的对象/接口来模拟,以便测试. 背景 前后端分离 前后端仅仅通过异步接口(AJAX/JSONP)来编程 前后端都 ...

  5. Java多线程编程实战指南(核心篇)读书笔记(五)

    (尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/76730459冷血之心的博客) 博主准备恶补一番Java高并发编程相 ...

  6. Java多线程编程实战指南(核心篇)读书笔记(四)

    (尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/76690961冷血之心的博客) 博主准备恶补一番Java高并发编程相 ...

  7. Java多线程编程实战指南(核心篇)读书笔记(三)

    (尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/76686044冷血之心的博客) 博主准备恶补一番Java高并发编程相 ...

  8. Java多线程编程实战指南(核心篇)读书笔记(二)

    (尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/76651408冷血之心的博客) 博主准备恶补一番Java高并发编程相 ...

  9. Java多线程编程实战指南(核心篇)读书笔记(一)

    (尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/76422930冷血之心的博客) 博主准备恶补一番Java高并发编程相 ...

随机推荐

  1. Pay Back(模拟)

    链接:https://ac.nowcoder.com/acm/contest/1086/C 题目描述 "Never a borrower nor a lender be." O h ...

  2. 三十三、www服务apache软件

    1.前面提到:www服务是一种网页服务,但是网页服务也是需要软件来支撑的,通过软件的形式展示需要的网页,返回给浏览器. www服务软件排名:http://w3techs.com/technologie ...

  3. [Algo] 281. Remove Spaces

    Given a string, remove all leading/trailing/duplicated empty spaces. Assumptions: The given string i ...

  4. winform显示word、ppt和pdf,用一个控件显示

    思路:都以pdf的格式展示,防止文件拷贝,所以要把word和ppt转换为pdf:展示用第三方组件O2S.Components.PDFView4NET.dll,破解版的下载链接:https://pan. ...

  5. bootstrap的button按钮点击之后会有蓝色边框怎么解决?

    .btn:focus,.btn:active:focus, .btn.active:focus,.btn.focus, .btn:active.focus,.btn.active.focus { ou ...

  6. iOS传感器集锦、飞机大战、开发调试工具、强制更新、Swift仿QQ空间头部等源码

    iOS精选源码 飞机大作战 MUPhotoPreview -简单易用的图片浏览器 LLDebugTool是一款针对开发者和测试者的调试工具,它可以帮... 多个UIScrollView.UITable ...

  7. php获取mysql大小

      查看指定数据库大小:  SELECT sum(DATA_LENGTH)+sum(INDEX_LENGTH) FROM information_schema.TABLES where    TABL ...

  8. 系统学习javaweb1----HTML语言1

    自我感受:HTML语言没想到也有这么大的学问,竟然能通过超链接标签直接访问百度,这可让我大吃一惊,我也得反思一下自己,上学期的java纯是混过来的,没有系统的学习过,感觉能通过期末考试都是侥幸,接下来 ...

  9. Java反射的实例

    JAVA反射机制是在运行状态中,对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;         这种动态获取的信息以及动态调用对象的方法的功能称为ja ...

  10. SPA(单页面web应用)和MPA(多页面web应用)的区别

    转:https://blog.csdn.net/amaniz/article/details/79203562 vue多页面应用开发请参见: https://github.com/amunamuna/ ...