如何在前端中使用protobufnode篇

46次阅读

共计 5309 个字符,预计需要花费 14 分钟才能阅读完成。

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

解析思路

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

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

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

预期效果

封装两个基础模块:

  • request.js: 用于根据接口名称、请求体、返回值类型,发起请求。
  • proto.js用于解析 proto, 将数据转换为二进制。

在项目中可以这样使用:

// lib/api.js 封装 API

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

/**
 * 
 * @param {* 请求数据} params
 *  getStudentList 是接口名称
 *  school.PBStudentListRsp 是定义好的返回 model
 * school.PBStudentListReq 是定义好的请求体 model
 */
exports.getStudentList = function getStudentList (params) {const req = proto.create('school.PBStudentListReq', params)
  return request('school.getStudentList', req, 'school.PBStudentListRsp')
}

// 项目中使用 lib/api.js
const api = require('../lib/api')
const req = {
  limit: 20,
  offset: 0
}
api.getStudentList(req).then((res) => {console.log(res)
}).catch(() => {// ...})

准备工作:

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

封装 proto.js

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

protobuf.load("awesome.proto", function(err, root) {if (err)
        throw err;
    var AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage");

    var payload = {awesomeField: "AwesomeString"};

    var message = AwesomeMessage.create(payload); 

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

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

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

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

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

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

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

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

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

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

// proto.js
const fs = require('fs')
const path = require('path')
const ProtoBuf = require('protobufjs')

let _proto = null

// 将所有的.proto 存放在_proto 中
function loadProtoDir (dirPath) {const files = fs.readdirSync(dirPath)

  const protoFiles = files
    .filter(fileName => fileName.endsWith('.proto'))
    .map(fileName => path.join(dirPath, fileName))
  _proto = ProtoBuf.loadSync(protoFiles).nested
  return _proto
}

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

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

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

const reqData = {a: '1'}
const message = PBMessage.create(reqData)
const reqBuffer = PBMessage.encode(message).finish()

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

let _proto = null

// 将所有的.proto 存放在_proto 中
function loadProtoDir (dirPath) {const files = fs.readdirSync(dirPath)

  const protoFiles = files
    .filter(fileName => fileName.endsWith('.proto'))
    .map(fileName => path.join(dirPath, fileName))
  _proto = ProtoBuf.loadSync(protoFiles).nested
  return _proto
}

// 根据 typeName 获取 PBMessage
function lookup (typeName) {if (!_.isString(typeName)) {throw new TypeError('typeName must be a string')
  }
  if (!_proto) {throw new TypeError('Please load proto before lookup')
  }
  return _.get(_proto, typeName)
}

function create (protoName, obj) {
  // 根据 protoName 找到对应的 message
  const model = lookup(protoName)
  if (!model) {throw new TypeError(`${protoName} not found, please check it again`)
  } 
  const req = model.create(obj)
  return model.encode(req).finish()}

module.exports = {
  lookup, // 这个方法将在 request 中会用到
  create,
  loadProtoDir
}

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

封装 request.js

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

const rp = require('request-promise') 
const proto = require('./proto.js')  // 上面我们封装好的 proto.js

/**
 * 
 * @param {* 接口名称} msgType 
 * @param {* proto.create()后的 buffer} requestBody 
 * @param {* 返回类型} responseType 
 */
function request (msgType, requestBody, responseType) {
  // 得到 api 的枚举值
  const _msgType = proto.lookup('framework.PBMessageType')[msgType]

  // PBMessageRequest 是公共请求体, 携带一些额外的 token 等信息,后端通过 type 获得接口名称,messageData 获得请求数据
  const PBMessageRequest = proto.lookup('framework.PBMessageRequest')
  const req = PBMessageRequest.encode({timeStamp: new Date().getTime(),
    type: _msgType,
    version: '1.0',
    messageData: requestBody,
    token: 'xxxxxxx'
  }).finish()

  // 发起请求,在 vue 中我们可以使用 axios 发起 ajax,但 node 端需要换一个,比如 "request"
  // 我这里推荐使用一个不错的库:"request-promise",它支持 promise
  const options = {
    method: 'POST',
    uri: 'http://your_server.com/api',
    body: req,
    encoding: null,
    headers: {'Content-Type': 'application/octet-stream'}
  }

  return rp.post(options).then((res) => {
    // 解析二进制返回值
    const  decodeResponse = proto.lookup('framework.PBMessageResponse').decode(res)
    const {resultInfo, resultCode} = decodeResponse
    if (resultCode === 0) {
      // 进一步解析解析 PBMessageResponse 中的 messageData
      const model = proto.lookup(responseType)
      let msgData = model.decode(decodeResponse.messageData)
      return msgData
    } else {throw new Error(`Fetch ${msgType} failed.`)
    }
  })
}

module.exports = request

使用

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

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

exports.getStudentList = function getStudentList (params) {const req = proto.create('school.PBStudentListReq', params)
  return request('school.getStudentList', req, 'school.PBStudentListRsp')
}

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

// test.js

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

const req = {
  limit: 20,
  offset: 0
}
api.getStudentList(req).then((res) => {console.log(res)
}).catch(() => {// ...})

最后

demo 源码

正文完
 0