现在的项目,ProtoBuf做前后端数据交互,对于我前端得个人体验来说
优点: 1.不需要接口文档,直接通过proto文件能够知道传参以及返回的参数,需要及时更新
缺点: 1.返回的接口数据,需要解析,并且打印,调试没有json那么方便
后端为什么要用protobuf 可参考这些文章:
如何在前端中使用protobuf(vue篇)
Protobuf 为啥比 JSON、XML 牛?
protobuf为什么那么快
简单来说就是通过protobuf通过1.压缩传输的数据,使数据变小,简单说下,就是删除一些没用的信息,采用自描述的方式记录 “类型”、“顺序”、“数据”。2.解析速度快,简单说下,就是读一个字节,不像json需要字符串解析
比如
json中的key是字符串,每个字符就会占据一个字节,所以像name这个key就会占据4个字节,但在protobuf中,tag使用二进制进行存储,一般只会占据一个字节。
比如在实际的传输过程中,会传递整数,我们知道整数在计算机当中占据4个字节,但是绝大部分的整数,比如价格,库存等,都是比较小的整数,实际用不了4个字节,像127这种数,在计算机中的二进制是:
00000000 00000000 00000000 01111111(4字节32位)
完全可以用最后1个字节来进行存储,protobuf当中定义了Varint这种数据类型,可以以不同的长度来存储整数,将数据进一步的进行了压缩
比如tag|value在
tag中含有value的数据类型的信息,而不同的数据类型有不同的大小,比如如果value是bool型,我们就知道肯定占了一个字节,程序从tag后面直接读一个字节就可以解析出value,非常快,而json则需要进行字符串解析才可以办到
1.看到proto文件先解析成前端工具能识别的
初次接触protobuf,服务端只告诉我这个活动用到那些proto,那么拿到这些proto文件,我们能干啥呢?
比如vue项目使用了webpack,webpack只认识json和js,那么第一步要做的就是将所有的proto文件解析成一个json文件,这样我们webpack才能认识它,那么有什么包能够实现这个操作呢,这个protobufjs-cli能够帮到你
1.1 下载protobufjs-cli
npm i protobufjs-cli --save
1.2 转化成json文件
"scripts": {
"proto": "npx pbjs -t json src/protobuf/protoFile/*.proto > src/protobuf/protoFile/proto.json",
},
1.3 效果
1.4 转化
protobufjs 很重要
npm i protobufjs --save
proto.ts
import { Root } from "protobufjs";
// https://www.npmjs.com/package/protobufjs
// 1.引入json数据
import protoJson from '../protoFile/proto.json' // webpack只认识js或者json,所以需要先将proto转成json
// 2.转化成pb结构对象(又要转回去)
export const root = Root.fromJSON(protoJson)
// 3.生成消息体/解析响应体
/**
* 生成消息体
* @param {string} description 请求体名字
* @param {object} data 请求数据
* @return {Buffer} 返回Buffer对象
*/
export const requestEncode = (reqDesction, data) => {
const reqDesctionData = root.lookupType(reqDesction); // 根据前端传入的reqDesction找到接口需要的参数
const err = reqDesctionData.verify(data) // 检验传入的参数格式是否正确
if (err) return false // 如果参数格式不正确就return, 当然ts也有保障
// creates a new message instance from a set of properties that satisfy
the requirements of a valid message
const message = reqDesctionData.create(data) // 创建成消息的格式
const buffer = reqDesctionData.encode(message).finish() // 解析成一个二进制流
return buffer
}
/**
* 解析响应体
* @param {string} resDesction 返回体名字
* @param {object} data 返回的数据
* @return {object} 解析完的数据
*/
export const responseDecode = (resDesction, data) => {
const resDesctionData = root.lookupType(resDesction); // 根据前端传入的reqDesction找到接口需要的参数
const err = resDesctionData.verify(data) // 检验传入的参数格式是否正确
if (err) return false // 如果参数格式不正确就return, 当然ts也有保障
const res = resDesctionData.decode(data)
const result = resDesctionData.toObject(res, {
defaults: true,
})
return result
}
requestEncode可以将传入的参数以及对应的请求体名字查到对应的传参,判断用户传入格式,并且处理成buffer,因为服务端定义了统一的消息体,里面的req就是必须要uint8Array[字节数组]的格式
responseDecode用于解析成前端常用的json格式
message xxxx {
string obj = 1; // 微服务端具体服务名
string func = 2; // 具体函数方法
bytes req = 3; // 请求数据包
map<string,string> opt = 4; //
}
2. 接口请求
假设我们请求一个时间戳,先写api
import request from "../component/request";
export function getRankList() {
return request({
reqDesction: 'GetTimestampReq',
resDesction: 'GetTimestampRes',
funcName: 'GetTimestamp',
data: {},
method: "post",
url: `/proxymsg`,
})
}
request.js
/**
* protobuf 接口 request封装
*/
import axios from 'axios'
import adapterRequest from "axios/lib/adapters/xhr";
import { root, requestEncode, responseDecode } from './proto.ts'
const isDebug = true
const Console = window.console // eslint no-console的原因,这里重新赋值一个新的console
// 定义参数
const REQUESTDOMAIN = 'xxxxx'// 接口域名
const token = 'xxxxx'
/**
* 适配器请求
* @param {object} config 请求的配置数据
使用该配置项目, 我们可以设置属于自己的请求方法.这里实际上传入的是调用Adapter的newConfig
*/
function selfAdapter (newConfig) {
// let arrayBuffer = uint8Array.buffer
// console.log(newConfig.data) ArrayBuffer ---ArrayBuffer 对象代表储存二进制数据的一段内存,它不能直接读写
newConfig.data = newConfig.rpcBuffer
//console.log(newConfig.data) Uint8Array --对象是 ArrayBuffer 的一个数据类型
return adapterRequest(newConfig)
}
const request = axios.create({
timeout: 10000,
responseType: 'arraybuffer',
// 数据流进行传输
headers: {
'Content-Type': 'application/protobuf',
},
baseURL: REQUESTDOMAIN,
})
// 请求拦截器
request.interceptors.request.use(
async config => {
if (!root.nested) {
return Promise.reject(new Error('错误的proto.json'))
}
const defaults = {
rpcInDesction: 'AAAA', // 服务端统一请求体
rpcOutDesction: 'BBBB', // 服务端统一返回体
}
const newConfig = Object.assign(defaults, config)
// 1.拿到请求的数据流
// 传入参数data,并且寻找GetTimestamp的请求需要的参数,看看是不是格式符合,并且包装成消息的格式
// 如果符合就传入,并且解析成一个二进制流
const reqBuffer = requestEncode(config.reqDesction, config.data || {}) // 请求数据 => 数据流
// 2.将请求数据包传入到服务端默认的请求体的格式,传入的数据要符合服务端规定的rpc接口规范和安全
const rpcBuffer = requestEncode(defaults.rpcInDesction, {
obj: 'XXXXXXXX', // 必须 微服务端具体服务名,这个一般是文件名+方法名,问后端吧
func: "GetTimestamp", // 必须 具体函数方法
// 以下参数不必须,但是传了格式必须对
req: reqBuffer, // 请求数据包
opt: Object.assign( // 其他配置,非必须,看后端要求
{
...,
"X-Token": token,
lang: "xx",
},
config.opt
),
});
// adapter源码直接调用,并且传入config
newConfig.rpcBuffer = rpcBuffer // 一定要赋值到newConfig上adapter调用才能拿到对应的rpcBuffer
newConfig.headers = { 'X-Token': token || '' }
newConfig.data = newConfig.rpcBuffer //1. Uint8Array 对象是 ArrayBuffer 的一个数据类型
// isArrayBufferView(newConfig.data) 为true
newConfig.adapter = selfAdapter // adapter实际上是最后一个执行的
return newConfig
},
error => {
return Promise.reject(error)
},
)
// 响应拦截器
request.interceptors.response.use(
res => {
const opts = res.config // 请求配置
if (res.status !== 200) {
const message = '请求异常'
const error = {
message,
description: res.data.message,
}
return Promise.reject(error)
} else {
// 解析处理返回体
const rpcOut = responseDecode(opts.rpcOutDesction, new Uint8Array(res.data))
if (rpcOut && rpcOut.ret === 0) {
res.data = rpcOut.rsp ? responseDecode(opts.resDesction, rpcOut.rsp) : {}
isDebug && Console.log(`${opts.funcName} res: `, res.data)
return res.data
} else {
isDebug && Console.log(`${opts.funcName} res: `, rpcOut)
const message = rpcOut.ret === 9999 ? '数据异常' : rpcOut.desc
const error = {
message,
description: rpcOut.desc,
allDescription: rpcOut,
}
return Promise.reject(error)
}
}
},
err => {
isDebug && Console.log(err)
const message = '网络异常'
const error = {
description: err.message,
message,
}
return Promise.reject(error)
},
)
export default request
上面要注意的点:
1.axios配置 数据返回的是流,Content-Type是application/protobuf
responseType: 'arraybuffer',
// 数据流进行传输
headers: {
'Content-Type': 'application/protobuf',
},
- rpcBuffer = requestEncode(defaults.rpcInDesction, {xxx})传参问题
xxx一定要严格按照后端定义的请求消息体来写,当然有的可以不传,但是传了的数据类型必须保持一致
3.newConfig.adapter = selfAdapter
只要设置了adapter源码里面是主动调用的,并且传入config(这里的config)就是newConfig.adapter中调用adapter的newConfig
- api的动态配置参数
下面可以通过直接api自动配置
obj: 'XXXXXXXX', // 必须 微服务端具体服务名,这个一般是文件名+方法名,问后端吧
func: "GetTimestamp", // 必须 具体函数方法
=>
通过配置,从config里面拿
obj: config.serverName, // 必须 微服务端具体服务名
func: config.funcName, // 必须 具体函数方法
api:
export function currentTimestamp() {
return request({
reqDesction: 'GetTimestampReq',
resDesction: 'GetTimestampRes',
funcName: 'GetTimestamp',
data: {},
method: "post",
url: `/proxymsg`,
serverName: 'xxxxxxxxxxxx'
})
}
5.为什么这里要用适配器
后端要求的是uint8Array格式的数据包, 但是适配器中的源码,对isArrayBufferView(newConfig.data)进行了处理,处理成了arrayBuffer的格式,所以这里需要重新覆盖