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

fengxianqi發表於2018-12-12

前端時間分享了一篇:如何在前端中使用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中定義了兩個名稱空間:frameworkschoolproto檔案原始碼

封裝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原始碼

相關文章