乐趣区

基于protobuf协议的mock方案实践

由于平时业务中,和后台之间的对接是基于 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 文件的姿势是这样的:

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 是怎么样的:

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 是这个样子的:

{
  "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 数据,我们可以大致的实现会是这样的:

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 里的,也可以是在当前命名空间下的,也可以是引用自其他命名空间下的。所以我们需要进一步改写成这样:

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 包,可以给别人使用,所以需要做一些额外的完善工作。假设如果只能生成假数据,是远远不够的,例如用户希望可以对字符串类型的数据进行自定义,又或者在接口级别上对每个接口返回的数据进行再修改,所以需要丰富我们的模块的配置项,暴露一些钩子函数给使用者。
最后使用起来是这样的:

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、最后我们封装成一个模块,暴露配置项的方式,用于业务开发过程中。

谢谢观看~

退出移动版