前端protobuf请求响应封装(axios)

1,693 阅读7分钟

现在的项目,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的数据类型的信息,而不同的数据类型有不同的大小,比如如果valuebool型,我们就知道肯定占了一个字节,程序从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 效果

1665473225097.png

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',
  },
  1. rpcBuffer = requestEncode(defaults.rpcInDesction, {xxx})传参问题

xxx一定要严格按照后端定义的请求消息体来写,当然有的可以不传,但是传了的数据类型必须保持一致

3.newConfig.adapter = selfAdapter

只要设置了adapter源码里面是主动调用的,并且传入config(这里的config)就是newConfig.adapter中调用adapter的newConfig

1665477090494.png

  1. 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的格式,所以这里需要重新覆盖

1665477394831.png

axios源码解析在这里!