Vue项目中使用protobuf

Protobuf简介

Google Protocol Buffer(简称Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。

有几个优点:

  1. 平台无关,语言无关,可扩展;
  2. 提供了友好的动态库,使用简单;
  3. 解析速度快,比对应的XML快约20-100倍;
  4. 序列化数据非常简洁、紧凑,与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语法的。使用requiremodule.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 }) } // ...
  • 需要打印数据的时候,打开浏览器的调试工具F12Console → 输入$debug=true → 回车Enter。再去请求接口就会打印出每次请求的参数值和返回结果
  • 默认不会打印。开启打印后想要关闭。打开浏览器的调试工具F12Console → 输入$debug=true → 回车Enter

创作不易,若本文对你有帮助,欢迎打赏支持作者!

 分享给好友: