How to use protobuf in front end

Posted by Branden Wagner on Thu, 21 Nov 2019 01:08:33 +0100

A while ago, I shared an article: How to use protobuf in front end Has been a lazy cancer attack and has dragged the node page to the present day.In the last share, many students had some discussions about why Protobuf should be used in front-end, indicating that the front-end is not suitable for protobuf.Our company uses protobuf together on ios, android and web, and I also mentioned some of the benefits and benefits in my previous share.If it's used by your company, or maybe later, my two shares may give you some inspiration.

Analytical thinking

Also use protobuf.js This library will be parsed.

As mentioned earlier, in vue, to avoid using the.proto file directly, you need to package all.proto files into.js for use.

On the node side, they can also be packaged into js files for processing.However, since the node side is a server-side environment that allows the existence of.proto, we can actually use it in an elegant way: directly parsing.

anticipate result

Encapsulates two basic modules:

  • request.js: Used to initiate a request based on the interface name, the body of the request, and the return value type.
  • proto.js is used to parse proto and convert data to binary.

You can use this in your project:

// lib/api.js wrapper API

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

/**
 * 
 * @param {* Request Data} params
 *  getStudentList Is Interface Name
 *  school.PBStudentListRsp Is a well-defined return model
 * school.PBStudentListReq Is a well-defined request body model
 */
exports.getStudentList = function getStudentList (params) {
  const req = proto.create('school.PBStudentListReq', params)
  return request('school.getStudentList', req, 'school.PBStudentListRsp')
}

// Use lib/api.js in your project
const api = require('../lib/api')
const req = {
  limit: 20,
  offset: 0
}
api.getStudentList(req).then((res) => {
  console.log(res)
}).catch(() => {
  // ...
})

Dead work:

Get ready How to use protobuf in front end Note that there are two namespaces defined in this proto: framework and school. proto file source

Encapsulate proto.js

Refer to the official documentation for how to convert an object to a 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();
});

It should be easier to understand: load awesome.proto first, then turn the data payload into the buffer we want.Both create and encode are methods provided by protobufjs.

If we only have one.proto file in our project, we can use it like an official document.
However, in actual projects, there are often many.proto files. If each PBMessage needs to know which.proto file to use first, it will be more cumbersome to use, so it needs to be encapsulated.
In the enumeration of interfaces given to us by the students on the server side, this is generally the case:

getStudentList = 0;    // Get a list of all students, PBStudentListReq => PBStudentListRsp

This only tells us that the request body for this interface is PBStudentListReq, and the return value is PBStudentListRsp, which is in a.proto file that is unknown.

For ease of use, we want to encapsulate a method such as:

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

We only need PBStudentListReq and dataObj as parameters when using PBStudentListReq, no matter which.proto file PBStudentListReq is in.
Here's a difficult point: how do you find your.proto by type?

The method is to put all.Protos in memory and get the corresponding type by name.

Write a loadProtoDir method to save all PROTOS in the _proto variable.

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

let _proto = null

// Store all.Proto in _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 is like a tree that we can traverse to find a specific type, or we can get it directly from other methods, such as the lodash.get() method, which supports obj['xx.xx.xx'] for values.

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

So we get PBMessages smoothly for all PROTOS based on their type. PBMessages include create, encode methods provided by proto buf.js, which we can use directly and convert object s to buffer s.

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

Organize it and encapsulate it into three functions for later convenience:

let _proto = null

// Store all.Proto in _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
}

// Get PBMessage from typeName
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) {
  // Find corresponding message based on protoName
  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, // This method will be used in request
  create,
  loadProtoDir
}

This requires that ProtoDir be loaded before using create and lookup and that all PROTOS be put into memory.

Encapsulate request.js

Here's a suggestion to take a look first MessageType.proto Which defines the interface enumeration, request body, and response body as agreed upon with the backend.

const rp = require('request-promise') 
const proto = require('./proto.js')  // The proto.js we encapsulated above

/**
 * 
 * @param {* Interface Name} msgType 
 * @param {* proto.create()Post buffer} requestBody 
 * @param {* Return Type} responseType 
 */
function request (msgType, requestBody, responseType) {
  // Get the enumerated value of the api
  const _msgType = proto.lookup('framework.PBMessageType')[msgType]

  // PBMessageRequest is a public requester that carries some additional information such as token, the back end gets the interface name through type, and messageData gets the request data
  const PBMessageRequest = proto.lookup('framework.PBMessageRequest')
  const req = PBMessageRequest.encode({
    timeStamp: new Date().getTime(),
    type: _msgType,
    version: '1.0',
    messageData: requestBody,
    token: 'xxxxxxx'
  }).finish()

  // Initiate a request, in vue we can use axios to initiate ajax, but the node side needs to be replaced, such as "request"
  // I recommend a good library here:'request-promise', which supports 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) => {
    // Resolve Binary Return Value
    const  decodeResponse = proto.lookup('framework.PBMessageResponse').decode(res)
    const { resultInfo, resultCode } = decodeResponse
    if (resultCode === 0) {
      // Further parsing messageData in PBMessageResponse
      const model = proto.lookup(responseType)
      let msgData = model.decode(decodeResponse.messageData)
      return msgData
    } else {
      throw new Error(`Fetch ${msgType} failed.`)
    }
  })
}

module.exports = request

Use

request.js and proto.js provide the underlying services, and for ease of use, we also encapsulate an api.js that defines all the API in the project.

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')
}

When using interfaces in your project, you only need require('lib/api'), not directly reference proto.js and request.js.

// test.js

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

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

Last

demo source

Topics: node.js Vue iOS Android axios