前段时间分享了一篇文章:如何在前端使用protobuf(vue篇),一直懒癌拖着node文章到目前为止。在上次的分享中,很多同学就“前端为什么要用protobuf”展开了一些讨论,说protobuf不适合做前端。我们公司在ios、android、web端都使用了protobuf。我在之前的分享中也谈到了一些好处和好处。如果你的公司也在用,或者以后可能会用到,我的两个分享可能会给你一些启发。解析思路也是使用protobuf.js库进行解析。之前说过,在vue中,为了避免直接使用.proto文件,需要将所有的.proto打包成.js来使用。在node端,也可以打包成js文件进行处理。但是node端是server端的环境,.proto的存在是完全允许的,所以其实我们可以有一种优雅的使用方式:直接解析。预期效果封装了两个基本模块:request.js:用于根据接口名称、请求体、返回值类型发起请求。proto.js用于解析proto并将数据转换为二进制。在项目中可以这样使用://lib/api.js封装APIconstrequest=require('./request')constproto=require('./proto')/****@param{*requestdata}params*getStudentList为接口名*school.PBStudentListRsp为定义的返回模型*school.PBStudentListReq为定义的请求体模型*/exports.getStudentList=functiongetStudentList(params){constreq=proto.create('school.PBStudentListReq',params)returnrequest('school.getStudentList',req,'school.PBStudentListRsp')}//lib/api.jsconstapi=require('../lib/api')constreq={限制:20,offset:0}api.getStudentList(req).then((res)=>{console.log(res)}).catch(()=>{//...})准备:如何准备前端使用protobuf(vue章节)中定义的一个.proto。请注意,此proto定义了两个命名空间:framework和school。proto文件源码包proto.js参考官方文档将object转buffer方法:protobuf.load("awesome.proto",function(err,root){if(err)throwerr;varAwesomeMessage=root.lookupType("awesomepackage.AwesomeMessage");varpayload={awesomeField:"AwesomeString"};varmessage=AwesomeMessage.create(payload);varbuffer=AwesomeMessage.encode(message).finish();});应该更容易理解:首先加载awesome.proto,然后将数据payload转换成我们想要的buffer。create和encode都是protobufjs提供的方法。如果我们项目中只有一个.proto文件,我们可以像官方文档一样使用。但是在实际项目中,往往会有很多.proto文件。如果每个PBMessage都需要知道自己在哪个.proto文件中,使用起来会很麻烦,所以需要封装。服务器同学给我们的接口枚举一般是这样的:getStudentList=0;//获取所有学生的列表,PBStudentListReq=>PBStudentListRsp这里只告诉了这个接口的请求体是PBStudentListReq,返回值是PBStudentListRsp,它们所在的.proto文件是未知的。为了使用方便,我们希望封装一个方法,比如:constreqBuffer=proto.create('school.PBStudentListReq',dataObj)我们在使用的时候只需要将PBStudentListReq和dataObj作为参数即可,不需要不需要关心文件中的.protoPBStudentListReq是哪个。这里有一个难点:如何根据类型找到.proto?方法是:把所有的.proto放到内存中,然后根据名字得到对应的类型。编写一个loadProtoDir方法将所有原型保存在_proto变量中。//proto.jsconstfs=require('fs')constpath=require('path')constProtoBuf=require('protobufjs')let_proto=null//将所有.proto存储在_proto中functionloadProtoDir(dirPath){constfiles=fs.readdirSync(dirPath)constprotoFiles=files.filter(fileName=>fileName.endsWith('.proto')).map(fileName=>path.join(dirPath,fileName))_proto=ProtoBuf.loadSync(protoFiles).nestedreturn_proto}_proto类似于一棵树,我们可以遍历这棵树找到具体的类型,或者直接通过其他方法获取,比如lodash.get()方法,它支持obj['xx.xx.xx']获取值。const_=require('lodash')constPBMessage=_.get(_proto,'school.PBStudentListReq')这样我们就可以根据类型成功获取所有proto中的PBMessage,PBMessage会由protobuf.js提供create、encode等方法,我们可以直接使用,将object转化为buffer。constreqData={a:'1'}constmessage=PBMessage.create(reqData)constreqBuffer=PBMessage.encode(message).finish()整理一下,为了后面使用方便,封装成三个函数:let_proto=null//将所有的.proto存储在_proto中fileName=>path.join(dirPath,fileName))_proto=ProtoBuf.loadSync(protoFiles).nestedreturn_proto}//根据typeName获取PBMessagefunctionlookup(typeName){if(!_.isString(typeName)){thrownewTypeError('typeNamemustbeastring')}if(!_proto){thrownewTypeError('Pleaseloadprotobeforelookup')}return_.get(_proto,typeName)}functioncreate(protoName,obj){//根据protoName找到对应的messageconstmodel=lookup(protoName)if(!model){thrownewTypeError(`${protoName}notfound,pleasecheckitagain`)}constreq=model.create(obj)returnmodel.encode(req).finish()}module.exportts={lookup,//这个方法会在request中使用create,loadProtoDir}这里要求在使用create和lookup之前,需要先loadProtoDir,把所有的proto放到内存包request.js中,这里建议先看一下MessageType.proto,里面定义了和后端约定好的接口枚举、请求体、响应体。constrp=require('request-promise')constproto=require('./proto.js')//proto.js/**我们上面封装了**@param{*interfacename}msgType*@param{*bufferafterproto.create()}requestBody*@param{*returntype}responseType*/functionrequest(msgType,requestBody,responseType){//获取api的枚举值const_msgType=proto.lookup('framework.PBMessageType')[msgType]//PBMessageRequest是一个公共请求体,携带一些额外的token等信息,后端通过type获取接口名,messageData获取请求数据constPBMessageRequest=proto.lookup('framework.PBMessageRequest')constreq=PBMessageRequest.encode({timeStamp:newDate().getTime(),type:_msgType,version:'1.0',messageData:requestBody,token:'xxxxxxx'}).finish()//发起请求,在vue我们可以使用axios来发起ajax,但是node端需要改一下,比如"request"//这里推荐使用一个不错的库:"request-promise",它支持promiseconstoptions={method:'POST',uri:'http://your_server.com/api',body:req,encoding:null,headers:{'Content-Type':'application/octet-stream'}}返回rp.post(options).then((res)=>{//解析二进制返回值constdecodeResponse=proto.lookup('framework.PBMessageResponse').decode(res)const{resultInfo,resultCode}=decodeResponseif(resultCode===0){//进一步解析解析PBMessageResponse中的messageDataconstmodel=proto.lookup(responseType)letmsgData=model.decode(decodeResponse.messageData)returnmsgData}else{thrownewError(`Fetch${msgType}failed.`)}})}module.exports=request使用request.js和proto.js提供底层服务。为了方便,我们还需要封装一个api.js,在项目中定义所有的apicon。request=require('./request')constproto=require('./proto')exports.getStudentList=functiongetStudentList(params){constreq=proto.create('school.PBStudentListReq',params)返回请求('school.getStudentList',req,'school.PBStudentListRsp')}在项目中使用该接口时,只需要require('lib/api'),不直接引用proto.js和request.js。//test.jsconstapi=require('../lib/api')constreq={limit:20,offset:0}api.getStudentList(req).then((res)=>{console.log(res)}).catch(()=>{//...})最后演示源代码
