前段時間分享了一篇:如何在前端中使用protobuf(vue篇),一直懶癌發做把node篇拖到了如今。上次分享中不少同窗就"前端爲何要用protobuf"展開了一些討論,表示前端不適合用
protobuf
。我司是ios、android、web幾個端都一塊兒用了protobuf,我也在以前的分享中講了其中的一些收益和好處。若是大家公司也用到,或者之後可能用到,個人這兩篇分享或許能給你一些啓發。前端
一樣是要使用protobuf.js這個庫來解析。vue
以前提到,在vue中,爲了不直接使用.proto
文件,須要將全部的.proto
打包成.js
來使用。node
而在node端,也能夠打包成js文件來處理。但node端是服務端環境了,徹底能夠容許.proto
的存在,因此其實咱們能夠有優雅的使用方式:直接解析。android
封裝兩個基礎模塊:ios
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中定義了兩個命名空間:framework
和school
。proto文件源碼git
參考下官方文檔將object轉化爲buffer的方法:github
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
轉變成咱們想要的buffer
。create
和encode
都是protobufjs提供的方法。web
若是咱們的項目中只有一個.proto
文件,咱們徹底能夠像官方文檔這樣用。 可是在實際項目中,每每是有不少個.proto
文件的,若是每一個PBMessage都要先知道在哪一個.proto
文件中,使用起來會比較麻煩,因此須要封裝一下。 服務端同窗給咱們的接口枚舉中通常是這樣的:ajax
getStudentList = 0; // 獲取全部學生的列表, PBStudentListReq => PBStudentListRsp
複製代碼
這裏只告訴了這個接口的請求體是PBStudentListReq
,返回值是PBStudentListRsp
,而它們所在的.proto
文件是不知道的。axios
爲了使用方便,咱們但願封裝一個方法,形如:
const reqBuffer = proto.create('school.PBStudentListReq', dataObj)
複製代碼
咱們使用時只須要以PBStudentListReq
和dataObj
做爲參數便可,無需關心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獲取到了PBMessage
,PBMessage
中會有protobuf.js提供的create
、encode
等方法,咱們能夠直接利用並將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
}
複製代碼
這裏要求,在使用create
和lookup
前,須要先loadProtoDir
,將全部的proto都放進內存。
這裏要建議先看一下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.js
和proto.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(() => {
// ...
})
複製代碼