由于平时业务中,和后台之间的对接是基于pb协议的,而pb协议中已经定义好了接口中的数据字段以及相应的数据类型,基于此实现一个mock假数据的方案,可以大大提高开发效率~
前言
protocol-buffer是一种数据传输格式,通过proto文件定义数据结构,编解码过程基于二进制,相比http协议更快,在内网服务器之间的数据通信以及客户端和服务器之间的通信会更有优势,不过这样的代价是双边需要维护同一份proto文件(下文简称为pb文件),如果那还不了解pb协议的话可以查看https://developers.google.cn/protocol-buffers/
由于所在团队业务开发过程中使用pb协议,因此下文将着重讲解mock方案的选取以及具体的实现过程。
方案选取
常见的mock数据方案很多,要如何去mock?在系统的哪一层去mock?是我们需要思考的问题之一,以下列出常见的几种mock方案:
- 1、前端直接hack请求接口,对fetch、ajax接口进行改写,不发出真实请求,直接前端页面内生成假数据;
- 2、前端页面配置代理,请求走进抓包工具,类似fiddler、whistle等,之后对包内容进行替换。(严格意义上不算是mock方案,因为数据不能随机和自动生成);
- 3、服务端生成mock数据,由于不希望对真实后台造成额外影响,我们可以在业务node中间层生成mock数据,真正要请求的时候则由node转发到后台;
- 4、单独搭建mock平台,所有项目所有接口都可以在mock系统上先配置好,接下来要做只是将请求转发到mock平台由mock平台响应数据即可。
最后,生成数据只需要借助于mockjs来生成即可,了解更多mockjs可以戳http://mockjs.com/
选取:由于协议不是普通的http协议,是基于pb协议的,而且我们需要通过解析pb协议获取到请求中的字段,所以我们可以在node中间层做这件事,而前两种方案请求在前端就结束了,无法解析pb协议,最后一种方案成本过高,于是我们选取了第三种方案。
方案实践
1、解析pb文件,获取AST
由于node中间层需要做转发工作,将前端的http请求封装成相应的pb协议的包发送给业务后台,所以我们可以在node中间层这里做一层mock,如果要走mock路径,我们只需要将解析pb文件,获取到文件相应的AST,之后生成接口(命令字)相应的数据即可。
解析pb文件获取AST已经有现成的一些工具可以给我们使用了,例如protobufjs 和 gulp-protobufjs,这里我们选取的是protobufjs,需要获取到pb文件的AST,以json的形式表示。github链接戳一戳:https://github.com/protobufjs/protobuf.js
利用protobuf解析一个pb文件的姿势是这样的:
1 2 3 4 5 6 7 | var pbjs = require("protobufjs/cli/pbjs"); // or require("protobufjs/cli").pbjs / .pbts pbjs.main([ "--target", "json", "./myproto.proto" ], function(err, output) { if (err) throw err; // do something with output }); |
通过这样的方式可以解析获取到AST相应的json,json是长这样的:
接下里我们以这个pb文件为例看看解析出来的AST是怎么样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package MY_NAMESPACE; import "common.proto"; message superMessage{ optional string testOne = 1; required subMessage testTwo = 2; required NS_COMM.test testThree = 3; message subMessage{ required string testName = 1; } } message otherMessage { optional int32 otherName = 1; } |
解析出来的JSON是这个样子的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | { "nested": { "TEST_NAMESPACE": { "nested": { "superMessage": { "fields": { "testOne": { "type": "string", "id": 1 }, "testTwo": { "rule": "required", "type": "subMessage", "id": 2 }, "testThree": { "rule": "required", "type": "NS_COMM.test", "id": 3 } }, "nested": { "subMessage": { "fields": { "testName": { "rule": "required", "type": "string", "id": 1 } } } } }, "otherMessage": { "fields": { "otherName": { "type": "int32", "id": 1 } } } } }, "NS_COMM": { .... .... } } } |
观察一下这个json,你会发现频繁的出现nested字段,protobufjs解析出来的AST利用nested来表示嵌套关系,第一层nested下是解析出来的所有的命名空间,命名空间下的nested嵌套message或enum类型,message类型下可以继续嵌套message或enum,以此类推,形成一个AST树来表示这种数据结构的层级关系。
2、根据AST生成mock数据
接下里需要做的是根据AST生成相应的mock数据,以命令字TEST_NAMESPACE.superMessage为例,假设需要生成该命令字相应的mock数据,我们可以大致的实现会是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const cmd = `TEST_NAMESPACE.superMessage` const message = findMessage(cmd) const result = handleMessage(message) function handleMessage(message){ const finalObject = {} for(let key in message.fields){ let type = message.fields[key].type finalObject[key] = getMockData(type) } return finalObject } function getMockData(field){ switch(field){ case "string":return String("test"); case "int32":return 1024 ; default: return undefined; } } |
如果只是简单的遍历一下key,似乎很简单,但是pb文件的规则却没有那么简单,由于允许嵌套message,message中的字段类型,除了普通类型string、int32以外,还可以是message或enum,其次,这些用户自定义类型可以是嵌套在message里的,也可以是在当前命名空间下的,也可以是引用自其他命名空间下的。所以我们需要进一步改写成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 | function handleMessage(message){ const finalObject = {} for(let key in message.fields){ let type = message.fields[key].type if(isNormalType(type)){ finalObject[key] = getMockData(type) }else{ let message = findMessage(type) finalObject[key] = handleMessage(message) } } return finalObject } |
可以看到里面有个findMessage方法,用于查找对应的message(也可能是enum),这个方法的查找流程是:
1、查找当前所在message下的nested,若无,则进行第二步;
2、查找当前所在的命名空间的nested,若无,则进行第三步;
3、查找所有命名空间下的nested,是否存在相应命令字,若不存在,抛出错误。
(具体的查找nested的过程,可以通过递归查找对应属性是否存在,为了偷懒笔者直接通过eval来查找对象上的属性,具体代码就不展示了)
解决了最主要的问题,接下来需要处理的就是其他语法规则:
1、针对enum类型的处理;
2、针对repeated语法的处理,返回数组;
3、针对extend、oneof、reserved以及map语法的处理。
3、丰富配置项,增加钩子函数
由于我们需要将这个封装成一个npm包,可以给别人使用,所以需要做一些额外的完善工作。假设如果只能生成假数据,是远远不够的,例如用户希望可以对字符串类型的数据进行自定义,又或者在接口级别上对每个接口返回的数据进行再修改,所以需要丰富我们的模块的配置项,暴露一些钩子函数给使用者。
最后使用起来是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | const mocker = require("pbmock") var req = {}, res = { send:()=>{ console.log("done!") } } var result = mocker({ cmd:"superMessage", whiteList:[ "superMessage" ], disabled:!process.env.development , entry:path=>path.resolve(__dirname,"./pb/mytest.proto"), configureType:{ "int32":mock=>{ return mock.Random.integer(-2,-1) }, "string":"oleiwa" }, hook:{ "superMessage":(source,Mock)=>{ source.code = 0; } }, times:2, logger:true, exposeVar:{ req, res, }, intercept:({req,res},data)=>{ res.send(data) } }) |
最后通过这样的方式,我们就解析了命令字superMessage,并且开心的进行了一些配置,返回了我们想要的数据。
接下来只需要把配置项提取到单独的一份config.js文件中,业务中我们只需要维护和修改配置文件即可。
总结
回顾一下本文所探讨的内容:
- 1、常见的mock方案以及基于pb协议mock方案的选取。
- 2、pb协议的mock方案具体实现过程,包括AST的生成,根据AST来mock数据,以及mock过程中所需要处理的若干问题。
- 3、最后我们封装成一个模块,暴露配置项的方式,用于业务开发过程中。
谢谢观看~