前段时间分享了一篇:
如何在前端中使用protobuf(vue篇),一直懒癌发做把node篇拖到了如今。上次分享中不少同窗就"前端为何要用protobuf"展开了一些讨论,表示前端不适合用
protobuf
。我司是ios、android、web几个端都一块儿用了protobuf,我也在以前的分享中讲了其中的一些收益和好处。若是大家公司也用到,或者之后可能用到,个人这两篇分享或许能给你一些启发。
一样是要使用protobuf.js这个库来解析。前端
以前提到,在vue中,为了不直接使用.proto
文件,须要将全部的.proto
打包成.js
来使用。vue
而在node端,也能够打包成js文件来处理。但node端是服务端环境了,彻底能够容许.proto
的存在,因此其实咱们能够有优雅的使用方式:直接解析。node
封装两个基础模块:android
request.js
: 用于根据接口名称、请求体、返回值类型,发起请求。proto.js
用于解析proto,将数据转换为二进制。在项目中能够这样使用:ios
// 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(() => { // ... })