Protobuf简介
Google Protocol Buffer
(简称Protobuf
)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。
有几个优点:
- 平台无关,语言无关,可扩展;
- 提供了友好的动态库,使用简单;
- 解析速度快,比对应的XML快约20-100倍;
- 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
思路
- 在使用
vue-cli
脚手架搭建的Vue
项目中使用 - 前端使用
protobuf.js
这个库来处理proto
文件 - 接口请求相关资料是通过
yaml
文件保存的。前端使用yaml-loader
处理 proto
文件和yaml
文件由后端开发人员维护,前端直接拿过来用。
步骤
插件安装
npm i axios protobufjs --save npm i yaml-loader --save-dev
命令添加
- 在
src/
目录下新建proto/
目录 - 将所有的
proto
文件复制到src/proto/
目录下 - 修改
package.json
文件。在scripts
下面添加命令
{
// ...
"scripts": {
// ...
"proto": "pbjs -t json-module -w commonjs -o src/proto/proto.js src/proto/*.proto"
},
// ...
}
备注:
- 通过此命令,可以将
src/proto/
目录下的所有proto
文件生成一个src/proto/proto.js
文件- 当然也可以生成
json
文件,但实践证明打包成js
模块比较好用-w
参数可以指定打包js的包装器,这里用的是commonjs
,即打包后的js
文件是nodejs
语法的。使用require
、module.exports
- 若想使用
es6
语法,可以将-w
参数的commonjs
修改成es6
。具体配置看protobuf.js
文档
yaml文件处理
yaml
文件示例
commands:
UploadCmd.ADVISOR_UPLOAD:
name: "UploadInfo"
url: "/advisor/upload"
cmd: 0x6401
comment: UploadInfo ADVISOR_UPLOAD port 8811 上传图片或视频 FileExt FileData UseFor 必填
UploadCmd.ADVISOR_UPLOAD_RSP:
name: "UploadInfo"
url: "/advisor/upload"
cmd: 0x6402
comment: ADVISOR_UPLOAD_RSP
- 在
src/
目录下新建axios/
目录 - 将
yaml
文件复制到src/axios/
目录下 - 修改
build/webpack.base.conf.js
文件,添加yaml-loader
配置
// ...
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.yaml$/,
loader: 'yaml-loader',
include: [resolve('src')]
},
// ...
]
},
// ...
}
引入文件代码
import yaml from 'json-loader!yaml-loader!./protobuf.yaml'
console.log(yaml)
ajax封装
在src/axios/
目录下新建文件request_pb.js
ajax
发起http
请求的整个流程:开始调用接口
-> request_pb.js将数据变成二进制
-> 前端真正发起请求
-> 后端返回二进制的数据
-> request_pb.js处理二进制数据
-> 获得数据对象
接口都是二进制的数据,所以需要设置axios
的请求头,使用arraybuffer
import axios from 'axios'
const httpService = axios.create({
timeout: 45000,
method: 'post',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/octet-stream'
},
responseType: 'arraybuffer'
})
http_base.proto
里面定义了与后端约定的接口枚举、请求体、响应体如下:
syntax = "proto3";
package advisor;
// ...
// 请求
message ReqBase
{
DeviceBase Dev = 1; // 设备信息
uint64 UserId = 2; // 用户id
uint32 Cmd = 3; // 命令字
uint64 MsgId = 4; // BussnissId,业务号,客户端发起,原样返回
string SessionId = 5; // SessionId
bytes PbBody = 6; // 数据内容
string MsgCtx = 7; // 请求描述
}
// 响应
message RetBase
{
int32 Code = 1; // 错误码
uint64 UserId = 2; // 用户id
uint32 Cmd = 3; // 命令字
uint64 MsgId = 4; // BussnissId,业务号,客户端发起,原样返回
bytes PbBody = 6; // 数据内容
string MsgCtx = 7; // 错误描述
}
// ...
在src/axios/request_pb.js
文件中根据约定好的请求体、响应体编写
// 请求体message
const PBMessageRequest = protoRoot.lookup('advisor.ReqBase')
// 响应体的message
const PBMessageResponse = protoRoot.lookup('advisor.RetBase')
// 构造公共请求体:PBMessageRequest
const reqData = {
Dev: {},
PbBody: {},
SessionId: '',
UserId: '',
Cmd: '',
MsgId: 0,
MsgCtx: ''
}
src/axios/request_pb.js
完整代码如下:
import axios from 'axios';
import protoRoot from '@/proto/proto'
import protobuf from 'protobufjs'
import store from '@/store'
import Msg from '@/utils/msg'
import { commands } from 'json-loader!yaml-loader!./protobuf.yaml'
const httpService = axios.create({
timeout: 45000,
method: 'post',
baseURL: process.env.urlApi_go,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/octet-stream'
},
responseType: 'arraybuffer'
})
// 请求体message
const PBMessageRequest = protoRoot.lookup('advisor.ReqBase')
// 响应体的message
const PBMessageResponse = protoRoot.lookup('advisor.RetBase')
// 将请求数据encode成二进制,encode是proto.js提供的方法
function transformRequest(data) {
const aa = PBMessageRequest.encode(data).finish()
return aa;
}
function isArrayBuffer(obj) {
return Object.prototype.toString.call(obj) === '[object ArrayBuffer]'
}
function transformResponseFactory(responseType, str) {
return function transformResponse(rawResponse) {
// 判断response是否是arrayBuffer
if (rawResponse == null || !isArrayBuffer(rawResponse)) {
return rawResponse
}
try {
const buf = protobuf.util.newBuffer(rawResponse)
// decode响应体
const decodedResponse = PBMessageResponse.decode(buf)
if (decodedResponse.Code === undefined || decodedResponse.Code === 0) {
if (decodedResponse.PbBody && responseType) {
const model = protoRoot.lookup(responseType)
decodedResponse.data = model.decode(decodedResponse.PbBody)
decodedResponse.data && (decodedResponse.data.code = 0)
}
} else {
decodedResponse.data = {}
decodedResponse.data.code = decodedResponse.Code
decodedResponse.data.msg = decodedResponse.Message
}
decodedResponse.cmd_str = str
return decodedResponse
} catch (err) {
return err
}
}
}
/**
* request
* @param {*} api 接口名称
* @param {*} params 请求体参数
*/
function request(api, params) {
const pbConstruct = protoRoot.lookup(commands[api].name)
const requestBody = pbConstruct.encode(params).finish()
// 构造公共请求体:PBMessageRequest
const reqData = {
Dev: {
UUID: store.state.deviceId ? store.state.deviceId : ''
},
PbBody: requestBody,
SessionId: store.state.sessionId ? store.state.sessionId : '',
UserId: store.state.user ? store.state.user.UserId : '',
Cmd: commands[api].cmd,
MsgId: 0,
MsgCtx: ''
}
// 将对象序列化成请求体实例
const req = PBMessageRequest.create(reqData)
// 这里用到axios的配置项:transformRequest和transformResponse
// transformRequest 发起请求时,调用transformRequest方法,目的是将req转换成二进制
// transformResponse 对返回的数据进行处理,目的是将二进制转换成真正的json数据
return httpService.post(commands[api].url, req, {
transformRequest,
transformResponse: transformResponseFactory(commands[api+'_RSP'].rspName, api)
}).then(({ data, status }) => {
// 对请求做处理
if (status !== 200) {
const err = new Error('服务器异常')
Msg.error('服务器异常', 3000);
throw err
} else if (data.Code === undefined || data.Code === 0) {
return data.data
} else if (data.Code === 13001 || data.Code === 13002 || data.Code === 13003) {
// session out
Msg.warning("Session timeout, sign in again!", 3000)
store.dispatch('setSessionOut')
store.dispatch('showLogin')
} else {
Msg.error(data.MsgCtx, 3000)
return data.data
}
}, (err) => {
throw err
})
}
// 在request下添加一个方法,方便用于处理请求参数
request.create = function (protoName, obj) {
const pbConstruct = protoRoot.lookup(protoName)
return pbConstruct.encode(obj).finish()
}
export default request
ajax调用
调用示例
import request from '@/axios/request_pb'
const params = {
WordKey: 'keyword',
Skip: 0,
Limit: 20
}
const res = await request('SearchCmd.FUZZY_SEARCH_ADVISOR', params)
优化
Protobuf
在数据传输过程中,如果某个参数是默认值,这个值是不传输的
前端拿到数据后,解析数据时。如果是默认值的参数不会挂载到我们解析后的数据对象中,而是在prototype
中
我们通过obj.xxx
也可以拿到
但是vue
等双向绑定的框架中,会出现问题
我们这里把默认值的参数也添加到解析后的数据对象中
这里Long
型数据的解析可能会有问题,Javascript
中没有那种数据类型。我们使用Long.js
做处理
- 修改
src/utils/utils.js
文件,添加convertLongToNumber
方法
import Long from 'long'
export function convertLongToNumber(source) {
if (!source || typeof source !== 'object') {
return source
}
let targetObj
if (Array.isArray(source)) {
targetObj = []
source.forEach(item => {
if (item && Long.isLong(item)) {
targetObj.push(item.toNumber())
} else if (item && typeof item === 'object') {
targetObj.push(convertLongToNumber(item))
} else {
targetObj.push(item)
}
})
} else {
targetObj = {}
Object.keys(source).forEach((keys) => {
if (source[keys] && Long.isLong(source[keys])) {
targetObj[keys] = source[keys].toNumber()
} else if (source[keys] && typeof source[keys] === 'object') {
targetObj[keys] = convertLongToNumber(source[keys])
} else {
targetObj[keys] = source[keys]
}
})
}
return targetObj
}
- 修改
src/axios/request_pb.js
文件
// ...
import { convertLongToNumber } from '@/utils/utils'
// ...
function transformResponseFactory(responseType, str) {
return function transformResponse(rawResponse) {
// ...
try {
// ...
if (decodedResponse.Code === undefined || decodedResponse.Code === 0) {
if (decodedResponse.PbBody && responseType) {
const model = protoRoot.lookup(responseType)
// decodedResponse.data = model.decode(decodedResponse.PbBody)
const message = model.decode(decodedResponse.PbBody)
// 自己解析数据,将默认值取出来
decodedResponse.data = model.toObject(message, {
enums: Number, // enums as string names
longs: Number, // longs as strings (requires long.js)
bytes: String, // bytes as base64 encoded strings
defaults: true, // includes default values
arrays: true, // populates empty arrays (repeated fields) even if defaults=false
objects: true, // populates empty objects (map fields) even if defaults=false
oneofs: true // includes virtual oneof fields set to the present field's name
})
decodedResponse.data && (decodedResponse.data.code = 0)
}
} else {
// ...
}
// ...
} catch (err) {
return err
}
}
}
// ...
function request(api, params) {
// ...
return httpService.post(commands[api].url, req, {
transformRequest,
transformResponse: transformResponseFactory(commands[api+'_RSP'].rspName, api)
}).then(({ data, status }) => {
// Long型数据有问题,循环遍历一下,将所有的 Long 转换成 Number
const result = convertLongToNumber(data.data)
// ...
}, (err) => {
throw err
})
}
// ...
其他
打印请求响应参数
正式环境中排查问题时,一般需要查看接口的请求参数和返回的结果值
Protobuf
是二进制的,我们通过浏览器调试工具中的network
看到的数据是二进制数据
我们通过console.log
打印出每个接口的请求参数和返回的结果值
一般情况下,不需要打印这些值;只要我们需要的时候打印出来看。这里我们做一个开关
- 修改
src/axios/request_pb.js
文件
// ...
function request(api, params) {
// ...
return httpService.post(commands[api].url, req, {
transformRequest,
transformResponse: transformResponseFactory(commands[api+'_RSP'].rspName, api)
}).then(({ data, status }) => {
if (window.$debug) {
console.log('%c request: %s', 'color: green;', api, params, data.data)
}
// ...
}, (err) => {
throw err
})
}
// ...
- 需要打印数据的时候,打开浏览器的调试工具
F12
→Console
→ 输入$debug=true
→ 回车Enter
。再去请求接口就会打印出每次请求的参数值和返回结果 - 默认不会打印。开启打印后想要关闭。打开浏览器的调试工具
F12
→Console
→ 输入$debug=true
→ 回车Enter
。
发表评论