searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

axios结合protobufjs指引

2024-06-07 09:49:59
27
0

概述

在翼文档的需求中,后端提议采用proto进行前后端数据的传输,其考虑有两点,其一是数据不是明文传输(二进制),从某种程度上增加安全性以及传输速率,其二便是proto文件可以直接作为前后端接口文档,提高开发效率和减少沟通成本。本文以axios + protobufjs作为基础,讲述如何在生产中使用。

流程

  • 前端首先借助protobufjs生成proto.js文件

  • 将请求体进行编码,生成二进制请求体,而本次需求,后端需要前端将二进制再进行转换成base64,所以需要多一步二进制转base64的操作

  • axios发送post请求,并通过transformRequest将第二步编码后的数据交给axios

  • 后端接收请求并处理,将处理结果以二进制或者base64的形式返回给axios,通过transformResponse进行解码

  • 页面根据json数据进行展示或者交互

proto文件

proto文件必须指定请求体的数据结构以及返回体的数据结构,一般由后端定义,常用在微服务中。但是通过protobufjs,我们纯前端也可以使用。

base.proto文件:

syntax = "proto3";
 
  // 包名,很重要
 
 package framework;
 
  import "google/protobuf/timestamp.proto";
  import "google/protobuf/empty.proto";
 
  option java_multiple_files = true;
  option java_outer_classname = "BaseProto";
  option java_package = "com.ctg.doc.portal.protobuf.generated";
 
  // 通用的请求数据
 
 message CommonParam {
    optional uint64 userId = 1;
    optional string loginToken = 2;
    optional uint32 clientId = 3;
    optional string deviceId = 4;
  }
 
  // 通用的返回数据
 
 message Response {
    uint32 resultCode = 1;
    optional bytes data = 2;
    optional string errorMsg = 3;
  }

user.proto文件:

syntax = "proto3";
 
  package framework;
 
  import "google/protobuf/timestamp.proto";
  import "google/protobuf/empty.proto";
  import "base.proto"; // 引入通用的proto文件
 

  option java_multiple_files = true;
  option java_outer_classname = "PortalAccountProto";
  option java_package = "com.ctg.doc.portal.protobuf.generated";
 
  // 查询用户的请求数据
 
 messageQueryUserInfoRequest {
    CommonParam commonParam = 1;
  }
  // 用户信息返回数据(data部分)
 
 message UserInfoData {
    uint64 userId = 1;
    string userName = 2;
    string headUrl = 3;
    string mobile = 4;
  }
  // 登录的请求数据
 
 message LoginRequest {
    CommonParam commonParam = 1;
    string mobile = 2;
    string password = 3;
  }
  // 登录的返回数据(data部分)
 
 message LoginData {
    UserInfoData userInfo = 1;
    string token = 2;
  }

上面的proto文件相信有计算机基础的都看得懂,基本就是对请求数据或者响应数据进行定义,其值是多少我们不用关心,只要了解属性便可。

所以我们在请求的时候,需要定义好三个最主要的元素

  • 请求url,这个也看后端,如果后端把所有的请求放在一个url处理,然后通过增加一个请求名来区分不同的请求的话则不需要。本次每个请求一个url,因此需要定义url

  • 请求体,例如上面的LoginRequest

  • 响应体,例如上面的Response以及LoginData,这里的LoginData就是Response的登录返回data定义

步骤

处理proto文件

处理proto文件我们需要借助protobufjs来处理,它会帮我们基于*.proto文件生成proto.js文件或者proto.json文件,我们就是通过proto.js或者proto.json进行数据的序列化和反序列化的,本文使用proto.js。

安装依赖

npm install protobufjs
  // OR
 
pnpm install protobufjs
  // OR
 
yarn add protobufjs

生成proto.js文件

在package.json中,配置脚本,在运行或者打包的时候,需要先执行这个脚本生成proto.js

"scripts": {
    "proto": "pbjs -t json-module -w commonjs -o src/proto/proto.js src/proto/*.proto"
  }

编码

import protoRoot from 'xxx/proto' // 引入proto.js文件
 

  function addPrefix(requestType: string) {
  // 注意这里的framework就是proto文件定义的包名
 
return 'framework.' + resquestType
  }
  // 将请求数据encode成二进制,然后再转为base64
 
 function transformRequestFactory(requestType: string) {
    const PBRequest = protoRoot.lookupType(addPrefix(requestType))
    return function (data: any) {
      console.log('params', data)
      return btoa(String.fromCharCode(...PBRequest.encode(data).finish())) // base64
 
    // return PBRequest.encode(data).finish() // 二进制
 
  }
  }

解码

import protoRoot from 'xxx/proto' // 引入proto.js文件
 
 
  function base64ToBuffer(base64: string) {
    const binary = atob(base64)
    const arrayBuffer = new ArrayBuffer(binary.length)
    const uint8 = new Uint8Array(arrayBuffer)
    for (let i = 0; i < uint8.length; i++) {
      uint8[i] = binary.charCodeAt(i)
    }
    return uint8
  }
   
  function transformResponseFactory(responseData?: string) {
    return function transformResponse(rawResponse: string, _: AxiosHeaders, status?: number) {
      if (status !== 200) {
        return
      }
      try {
        // decode响应体
 
      const buf = base64ToBuffer(rawResponse)
        // 这里的Response和上面base.proto定义的通用响应体保持一致
 
      const PBResponse = protoRoot.lookupType(addPrefix('Response'))
        const decodedResponse = PBResponse.decode(buf)
        const result = PBResponse.toObject(decodedResponse, {
          defaults: true
        })
        // 上面定义的base.proto的通用返回体中data的定义是个二进制,所以我们需要对data再进行一次解码
 
      if (result.data && responseData) {
          const model = protoRoot.lookupType(addPrefix(responseData))
          result.data =model.decode(result.data).toJSON()
        }
        return result
      } catch (error) {
        Promise.reject(error)
      }
    }
  }

封装axios

import axios, { AxiosHeaders } from 'axios'
   
  const httpService = axios.create({
    timeout: 45000,
    method: 'post',
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Content-Type': 'application/octet-stream' // important
 
  },
    // responseType: 'arraybuffer', // 根据需要打开,如果你想让axios自动转为arraybuffer,或者后端本来就是返回二进制,这可以打开
 
  baseURL: process.env.VUE_APP_API_PREFIX
  })
   
  function request<T = any>(url: string, requestType: string, responseData?: string, params?: { [key: string]: any }) {
    if (!url.startsWith('/')) {
      url += '/'
    }
    const PBRequest = protoRoot.lookupType(addPrefix(requestType))
    const reqData = {
      commonParam: {
        userId: localStorage.getItem('uid') || '',
        loginToken: localStorage.getItem('token') || '',
        clientId: 1
      },
      ...(params || {})
    }
    // 将对象序列化成请求体实例
 
  const req = PBRequest.create(reqData)
   
    return new Promise<BaseResponse<T>>((resolve, reject) => {
      httpService
        .post<T>(url, req, {
          transformRequest: transformRequestFactory(requestType),
          transformResponse: transformResponseFactory(responseData)
        })
        .then(
          ({ data }: any) => {
            // 可以根据data.resultCode进行业务异常的通用处理,比如业务繁忙(限流导致),服务器异常等等 
 
          resolve(data)
          },
          error => {
            const { response } = error
            if (response?.status !== 200) {
              const msg = '请求异常'
              message.error(msg)
              const error = {
                msg,
                code: SERVER_FAILED_CODE
              }
              return reject(error)
            }
            reject(error)
          }
        )
    })
  }

编写接口

export function login(params: { mobile: string; password: string }) {
    return request<LoginResponse>('/account/login/mobilePassword', 'LoginRequest', 'LoginData', params)
  }

总结

我们可以通过protobufjs来实现用proto文件进行前后端的数据传输,其一般为二进制的方式进行传输,传输的大小和安全性比传统的优越,且后端不需要额外编写接口文档,提高开发效率和沟通成本。

但也有缺点,由于请求体和响应体是二进制的格式,所以调试或者定位问题的时候会比较麻烦,而且mock数据也会相对麻烦点。在开发模式下,我们可以通console.log打印出来,但是线上环境就行不通,会某种程度上造成定位问题效率低。

整体上来说,肯定是利大于弊。

0条评论
0 / 1000
c****x
1文章数
0粉丝数
c****x
1 文章 | 0 粉丝
c****x
1文章数
0粉丝数
c****x
1 文章 | 0 粉丝
原创

axios结合protobufjs指引

2024-06-07 09:49:59
27
0

概述

在翼文档的需求中,后端提议采用proto进行前后端数据的传输,其考虑有两点,其一是数据不是明文传输(二进制),从某种程度上增加安全性以及传输速率,其二便是proto文件可以直接作为前后端接口文档,提高开发效率和减少沟通成本。本文以axios + protobufjs作为基础,讲述如何在生产中使用。

流程

  • 前端首先借助protobufjs生成proto.js文件

  • 将请求体进行编码,生成二进制请求体,而本次需求,后端需要前端将二进制再进行转换成base64,所以需要多一步二进制转base64的操作

  • axios发送post请求,并通过transformRequest将第二步编码后的数据交给axios

  • 后端接收请求并处理,将处理结果以二进制或者base64的形式返回给axios,通过transformResponse进行解码

  • 页面根据json数据进行展示或者交互

proto文件

proto文件必须指定请求体的数据结构以及返回体的数据结构,一般由后端定义,常用在微服务中。但是通过protobufjs,我们纯前端也可以使用。

base.proto文件:

syntax = "proto3";
 
  // 包名,很重要
 
 package framework;
 
  import "google/protobuf/timestamp.proto";
  import "google/protobuf/empty.proto";
 
  option java_multiple_files = true;
  option java_outer_classname = "BaseProto";
  option java_package = "com.ctg.doc.portal.protobuf.generated";
 
  // 通用的请求数据
 
 message CommonParam {
    optional uint64 userId = 1;
    optional string loginToken = 2;
    optional uint32 clientId = 3;
    optional string deviceId = 4;
  }
 
  // 通用的返回数据
 
 message Response {
    uint32 resultCode = 1;
    optional bytes data = 2;
    optional string errorMsg = 3;
  }

user.proto文件:

syntax = "proto3";
 
  package framework;
 
  import "google/protobuf/timestamp.proto";
  import "google/protobuf/empty.proto";
  import "base.proto"; // 引入通用的proto文件
 

  option java_multiple_files = true;
  option java_outer_classname = "PortalAccountProto";
  option java_package = "com.ctg.doc.portal.protobuf.generated";
 
  // 查询用户的请求数据
 
 messageQueryUserInfoRequest {
    CommonParam commonParam = 1;
  }
  // 用户信息返回数据(data部分)
 
 message UserInfoData {
    uint64 userId = 1;
    string userName = 2;
    string headUrl = 3;
    string mobile = 4;
  }
  // 登录的请求数据
 
 message LoginRequest {
    CommonParam commonParam = 1;
    string mobile = 2;
    string password = 3;
  }
  // 登录的返回数据(data部分)
 
 message LoginData {
    UserInfoData userInfo = 1;
    string token = 2;
  }

上面的proto文件相信有计算机基础的都看得懂,基本就是对请求数据或者响应数据进行定义,其值是多少我们不用关心,只要了解属性便可。

所以我们在请求的时候,需要定义好三个最主要的元素

  • 请求url,这个也看后端,如果后端把所有的请求放在一个url处理,然后通过增加一个请求名来区分不同的请求的话则不需要。本次每个请求一个url,因此需要定义url

  • 请求体,例如上面的LoginRequest

  • 响应体,例如上面的Response以及LoginData,这里的LoginData就是Response的登录返回data定义

步骤

处理proto文件

处理proto文件我们需要借助protobufjs来处理,它会帮我们基于*.proto文件生成proto.js文件或者proto.json文件,我们就是通过proto.js或者proto.json进行数据的序列化和反序列化的,本文使用proto.js。

安装依赖

npm install protobufjs
  // OR
 
pnpm install protobufjs
  // OR
 
yarn add protobufjs

生成proto.js文件

在package.json中,配置脚本,在运行或者打包的时候,需要先执行这个脚本生成proto.js

"scripts": {
    "proto": "pbjs -t json-module -w commonjs -o src/proto/proto.js src/proto/*.proto"
  }

编码

import protoRoot from 'xxx/proto' // 引入proto.js文件
 

  function addPrefix(requestType: string) {
  // 注意这里的framework就是proto文件定义的包名
 
return 'framework.' + resquestType
  }
  // 将请求数据encode成二进制,然后再转为base64
 
 function transformRequestFactory(requestType: string) {
    const PBRequest = protoRoot.lookupType(addPrefix(requestType))
    return function (data: any) {
      console.log('params', data)
      return btoa(String.fromCharCode(...PBRequest.encode(data).finish())) // base64
 
    // return PBRequest.encode(data).finish() // 二进制
 
  }
  }

解码

import protoRoot from 'xxx/proto' // 引入proto.js文件
 
 
  function base64ToBuffer(base64: string) {
    const binary = atob(base64)
    const arrayBuffer = new ArrayBuffer(binary.length)
    const uint8 = new Uint8Array(arrayBuffer)
    for (let i = 0; i < uint8.length; i++) {
      uint8[i] = binary.charCodeAt(i)
    }
    return uint8
  }
   
  function transformResponseFactory(responseData?: string) {
    return function transformResponse(rawResponse: string, _: AxiosHeaders, status?: number) {
      if (status !== 200) {
        return
      }
      try {
        // decode响应体
 
      const buf = base64ToBuffer(rawResponse)
        // 这里的Response和上面base.proto定义的通用响应体保持一致
 
      const PBResponse = protoRoot.lookupType(addPrefix('Response'))
        const decodedResponse = PBResponse.decode(buf)
        const result = PBResponse.toObject(decodedResponse, {
          defaults: true
        })
        // 上面定义的base.proto的通用返回体中data的定义是个二进制,所以我们需要对data再进行一次解码
 
      if (result.data && responseData) {
          const model = protoRoot.lookupType(addPrefix(responseData))
          result.data =model.decode(result.data).toJSON()
        }
        return result
      } catch (error) {
        Promise.reject(error)
      }
    }
  }

封装axios

import axios, { AxiosHeaders } from 'axios'
   
  const httpService = axios.create({
    timeout: 45000,
    method: 'post',
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Content-Type': 'application/octet-stream' // important
 
  },
    // responseType: 'arraybuffer', // 根据需要打开,如果你想让axios自动转为arraybuffer,或者后端本来就是返回二进制,这可以打开
 
  baseURL: process.env.VUE_APP_API_PREFIX
  })
   
  function request<T = any>(url: string, requestType: string, responseData?: string, params?: { [key: string]: any }) {
    if (!url.startsWith('/')) {
      url += '/'
    }
    const PBRequest = protoRoot.lookupType(addPrefix(requestType))
    const reqData = {
      commonParam: {
        userId: localStorage.getItem('uid') || '',
        loginToken: localStorage.getItem('token') || '',
        clientId: 1
      },
      ...(params || {})
    }
    // 将对象序列化成请求体实例
 
  const req = PBRequest.create(reqData)
   
    return new Promise<BaseResponse<T>>((resolve, reject) => {
      httpService
        .post<T>(url, req, {
          transformRequest: transformRequestFactory(requestType),
          transformResponse: transformResponseFactory(responseData)
        })
        .then(
          ({ data }: any) => {
            // 可以根据data.resultCode进行业务异常的通用处理,比如业务繁忙(限流导致),服务器异常等等 
 
          resolve(data)
          },
          error => {
            const { response } = error
            if (response?.status !== 200) {
              const msg = '请求异常'
              message.error(msg)
              const error = {
                msg,
                code: SERVER_FAILED_CODE
              }
              return reject(error)
            }
            reject(error)
          }
        )
    })
  }

编写接口

export function login(params: { mobile: string; password: string }) {
    return request<LoginResponse>('/account/login/mobilePassword', 'LoginRequest', 'LoginData', params)
  }

总结

我们可以通过protobufjs来实现用proto文件进行前后端的数据传输,其一般为二进制的方式进行传输,传输的大小和安全性比传统的优越,且后端不需要额外编写接口文档,提高开发效率和沟通成本。

但也有缺点,由于请求体和响应体是二进制的格式,所以调试或者定位问题的时候会比较麻烦,而且mock数据也会相对麻烦点。在开发模式下,我们可以通console.log打印出来,但是线上环境就行不通,会某种程度上造成定位问题效率低。

整体上来说,肯定是利大于弊。

文章来自个人专栏
前端开发经验
1 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0