如何在前端中使用protobuf(node篇)

Posted 前端实战笔录

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在前端中使用protobuf(node篇)相关的知识,希望对你有一定的参考价值。

由于微信不允许外部链接,你需要点击页尾左下角的“阅读原文” ,才能访问文中的链接。

前段时间分享了一篇:如何在前端中使用protobuf(vue篇),一直懒癌发作把node篇拖到了现在。上次分享中很多同学就"前端为什么要用protobuf"展开了一些讨论,表示前端不适合用 protobuf。我司是iosandroid、web几个端都一起用了protobuf,我也在之前的分享中讲了其中的一些收益和好处。如果你们公司也用到,或者以后可能用到,我的这两篇分享或许能给你一些启发。

解析思路

同样是要使用protobuf.js这个库来解析。

之前提到,在vue中,为了避免直接使用 .proto文件,需要将所有的 .proto打包成 .js来使用。

而在node端,也可以打包成js文件来处理。但node端是服务端环境了,完全可以允许 .proto的存在,所以其实我们可以有优雅的使用方式:直接解析。

预期效果

封装两个基础模块:

  • request.js: 用于根据接口名称、请求体、返回值类型,发起请求。

  • proto.js用于解析proto,将数据转换为二进制。在项目中可以这样使用:

 
   
   
 
  1. // lib/api.js 封装API

  2. const request = require('./request')

  3. const proto = require('./proto')

  4. /**

  5. *

  6. * @param {* 请求数据} params

  7. * getStudentList 是接口名称

  8. * school.PBStudentListRsp 是定义好的返回model

  9. * school.PBStudentListReq 是定义好的请求体model

  10. */

  11. exports.getStudentList = function getStudentList (params) {

  12. const req = proto.create('school.PBStudentListReq', params)

  13. return request('school.getStudentList', req, 'school.PBStudentListRsp')

  14. }

  15. // 项目中使用lib/api.js

  16. const api = require('../lib/api')

  17. const req = {

  18. limit: 20,

  19. offset: 0

  20. }

  21. api.getStudentList(req).then((res) => {

  22. console.log(res)

  23. }).catch(() => {

  24. // ...

  25. })

准备工作:

准备如何在前端中使用protobuf(vue篇)中定义好的一份 .proto,注意这份proto中定义了两个命名空间:frameworkschool。proto文件源码

封装proto.js

参考下官方文档将object转化为buffer的方法:

 
   
   
 
  1. protobuf.load("awesome.proto", function(err, root) {

  2. if (err)

  3. throw err;

  4. var AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage");

  5. var payload = { awesomeField: "AwesomeString" };

  6. var message = AwesomeMessage.create(payload);

  7. var buffer = AwesomeMessage.encode(message).finish();

  8. });

应该比较容易理解:先load awesome.proto,然后将数据 payload转变成我们想要的 buffercreateencode都是protobufjs提供的方法。

如果我们的项目中只有一个 .proto文件,我们完全可以像官方文档这样用。但是在实际项目中,往往是有很多个 .proto文件的,如果每个PBMessage都要先知道在哪个 .proto文件中,使用起来会比较麻烦,所以需要封装一下。服务端同学给我们的接口枚举中一般是这样的:

 
   
   
 
  1. getStudentList = 0; // 获取所有学生的列表, PBStudentListReq => PBStudentListRsp

这里只告诉了这个接口的请求体是 PBStudentListReq,返回值是 PBStudentListRsp,而它们所在的 .proto文件是不知道的。

为了使用方便,我们希望封装一个方法,形如:

 
   
   
 
  1. const reqBuffer = proto.create('school.PBStudentListReq', dataObj)

我们使用时只需要以 PBStudentListReqdataObj作为参数即可,无需关心 PBStudentListReq是在哪个 .proto文件中。这里有个难点:如何根据类型来找到所在的 .proto呢?

方法是:把所有的 .proto放进内存中,然后根据名称获取对应的类型。

写一个loadProtoDir方法,把所有的proto保存在 _proto变量中。

 
   
   
 
  1. // proto.js

  2. const fs = require('fs')

  3. const path = require('path')

  4. const ProtoBuf = require('protobufjs')

  5. let _proto = null

  6. // 将所有的.proto存放在_proto中

  7. function loadProtoDir (dirPath) {

  8. const files = fs.readdirSync(dirPath)

  9. const protoFiles = files

  10. .filter(fileName => fileName.endsWith('.proto'))

  11. .map(fileName => path.join(dirPath, fileName))

  12. _proto = ProtoBuf.loadSync(protoFiles).nested

  13. return _proto

  14. }

_proto类似一颗树,我们可以遍历这棵树找到具体的类型,也可以通过其他方法直接获取,比如 lodash.get()方法,它支持 obj['xx.xx.xx']这样的形式来取值。

 
   
   
 
  1. const _ = require('lodash')

  2. const PBMessage = _.get(_proto, 'school.PBStudentListReq')

这样我们就拿到了顺利地根据类型在所有的proto获取到了 PBMessagePBMessage中会有protobuf.js提供的 createencode等方法,我们可以直接利用并将object转成buffer。

 
   
   
 
  1. const reqData = {a: '1'}

  2. const message = PBMessage.create(reqData)

  3. const reqBuffer = PBMessage.encode(message).finish()

整理一下,为了后面使用方便,封装成三个函数:

 
   
   
 
  1. let _proto = null

  2. // 将所有的.proto存放在_proto中

  3. function loadProtoDir (dirPath) {

  4. const files = fs.readdirSync(dirPath)

  5. const protoFiles = files

  6. .filter(fileName => fileName.endsWith('.proto'))

  7. .map(fileName => path.join(dirPath, fileName))

  8. _proto = ProtoBuf.loadSync(protoFiles).nested

  9. return _proto

  10. }

  11. // 根据typeName获取PBMessage

  12. function lookup (typeName) {

  13. if (!_.isString(typeName)) {

  14. throw new TypeError('typeName must be a string')

  15. }

  16. if (!_proto) {

  17. throw new TypeError('Please load proto before lookup')

  18. }

  19. return _.get(_proto, typeName)

  20. }

  21. function create (protoName, obj) {

  22. // 根据protoName找到对应的message

  23. const model = lookup(protoName)

  24. if (!model) {

  25. throw new TypeError(`${protoName} not found, please check it again`)

  26. }

  27. const req = model.create(obj)

  28. return model.encode(req).finish()

  29. }

  30. module.exports = {

  31. lookup, // 这个方法将在request中会用到

  32. create,

  33. loadProtoDir

  34. }

这里要求,在使用 createlookup前,需要先 loadProtoDir,将所有的proto都放进内存。

封装request.js

这里要建议先看一下 MessageType.proto,其中定义了与后端约定的接口枚举、请求体、响应体。

 
   
   
 
  1. const rp = require('request-promise')

  2. const proto = require('./proto.js') // 上面我们封装好的proto.js

  3. /**

  4. *

  5. * @param {* 接口名称} msgType

  6. * @param {* proto.create()后的buffer} requestBody

  7. * @param {* 返回类型} responseType

  8. */

  9. function request (msgType, requestBody, responseType) {

  10. // 得到api的枚举值

  11. const _msgType = proto.lookup('framework.PBMessageType')[msgType]

  12. // PBMessageRequest是公共请求体,携带一些额外的token等信息,后端通过type获得接口名称,messageData获得请求数据

  13. const PBMessageRequest = proto.lookup('framework.PBMessageRequest')

  14. const req = PBMessageRequest.encode({

  15. timeStamp: new Date().getTime(),

  16. type: _msgType,

  17. version: '1.0',

  18. messageData: requestBody,

  19. token: 'xxxxxxx'

  20. }).finish()

  21. // 发起请求,在vue中我们可以使用axios发起ajax,但node端需要换一个,比如"request"

  22. // 我这里推荐使用一个不错的库:"request-promise",它支持promise

  23. const options = {

  24. method: 'POST',

  25. uri: 'http://your_server.com/api',

  26. body: req,

  27. encoding: null,

  28. headers: {

  29. 'Content-Type': 'application/octet-stream'

  30. }

  31. }

  32. return rp.post(options).then((res) => {

  33. // 解析二进制返回值

  34. const decodeResponse = proto.lookup('framework.PBMessageResponse').decode(res)

  35. const { resultInfo, resultCode } = decodeResponse

  36. if (resultCode === 0) {

  37. // 进一步解析解析PBMessageResponse中的messageData

  38. const model = proto.lookup(responseType)

  39. let msgData = model.decode(decodeResponse.messageData)

  40. return msgData

  41. } else {

  42. throw new Error(`Fetch ${msgType} failed.`)

  43. }

  44. })

  45. }

  46. module.exports = request

使用

request.jsproto.js提供底层的服务,为了使用方便,我们还要封装一个 api.js,定义项目中所有的api。

 
   
   
 
  1. const request = require('./request')

  2. const proto = require('./proto')

  3. exports.getStudentList = function getStudentList (params) {

  4. const req = proto.create('school.PBStudentListReq', params)

  5. return request('school.getStudentList', req, 'school.PBStudentListRsp')

  6. }

在项目中使用接口时,只需要 require('lib/api'),不直接引用proto.js和request.js。

 
   
   
 
  1. // test.js

  2. const api = require('../lib/api')

  3. const req = {

  4. limit: 20,

  5. offset: 0

  6. }

  7. api.getStudentList(req).then((res) => {

  8. console.log(res)

  9. }).catch(() => {

  10. // ...

  11. })

最后

demo源码


以上是关于如何在前端中使用protobuf(node篇)的主要内容,如果未能解决你的问题,请参考以下文章

案例篇:利用ProtoBuf文件,一键生成Java代码

一个前端开发者换电脑的过程(node篇)

Protobuf 和 Node.JS 库错误

长文干货 | 如何利用Google的protobuf,来实现自己的RPC框架

Linux搭建服务器Node+Nginx+Tomcat+Redis+Oracle CentOS篇

netty案例,netty4.1中级拓展篇二《Netty使用Protobuf传输数据》