如何在前端中使用protobuf(vue篇)

前言

因为目前公司采用了ProtoBuf作先后端数据交互,进公司以来一直用的是公司大神写好的基础库,彻底不了解底层是如何解析的,一旦报错只能求人,做为一只还算有钻研精神的猿,应该去了解一下底层的实现,在这里记录一下学习过程。css

Protobuf简单介绍

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

有几个优势:前端

  • 1.平台无关,语言无关,可扩展;
  • 2.提供了友好的动态库,使用简单;
  • 3.解析速度快,比对应的XML快约20-100倍;
  • 4.序列化数据很是简洁、紧凑,与XML相比,其序列化以后的数据量约为1/3到1/10。

我的感觉: 先后端数据传输用json仍是protobuf其实对开发来讲没啥区别,protobuf最后仍是要解析成json才能用。我的以为比较好的几点是:vue

  • 1.先后端均可以直接在项目中使用protobuf,不用再额外去定义model;
  • 2.protobuf能够直接做为先后端数据和接口的文档,大大减小了沟通成本;

没有使用protobuf以前,后端语言定义的接口和字段,前端是不能直接使用的,先后端沟通每每须要维护一份接口文档,若是后端字段有改动,须要去修改文档并通知前端,有时候文档更新不及时或容易遗漏,沟通成本比较大。 使用protobuf后,protobuf文件由后端统必定义,protobuf直接能够做为文档,前端只需将protobuf文件拷贝进前端项目便可。若是后端字段有改动,只需通知前端更新protobuf文件便可,由于后端是直接使用了protobuf文件,所以protobuf文件通常是不会出现遗漏或错误的。久而久之,团队合做效率提高是明显的。node

废话了一大堆,下面进入正题。 我这里讲的主要是在vue中的使用,是目前本人所在的公司项目实践,你们能够当作参考。ios

思路

前端中须要使用 protobuf.js 这个库来处理proto文件。git

protobuf.js 提供了几种方式来处理proto。github

  • 直接解析,如protobuf.load("awesome.proto", function(err, root) {...})
  • 转化为JSON或js后使用,如protobuf.load("awesome.json", function(err, root) {...})
  • 其余

众所周知,vue项目build后生成的dist目录中只有html,css,js,images等资源,并不会有.proto文件的存在,所以须要用protobuf.js这个库将*.proto处理成*.js*.json,而后再利用库提供的方法来解析数据,最后获得数据对象。chrome

PS: 实践发现,转化为js文件会更好用一些,转化后的js文件直接在原型链上定义了一些方法,很是方便。所以后面将会是使用这种方法来解析proto。vue-cli

预期目标

在项目中封装一个request.js模块,但愿能像下面这样使用,调用api时只需指定请求和响应的model,而后传递请求参数,不需关心底层是如何解析proto的,api返回一个Promise对象:

// /api/student.js 定义接口的文件
import request from '@/lib/request'

// params是object类型的请求参数
// school.PBStudentListReq 是定义好的请求体model
// school.PBStudentListRsp 是定义好的响应model
// getStudentList 是接口名称
export function getStudentList (params) {
  const req = request.create('school.PBStudentListReq', params)
  return request('getStudentList', req, 'school.PBStudentListRsp')
}

// 在HelloWorld.vue中使用
import { getStudentList } from '@/api/student'
export default {
  name: 'HelloWorld',
  created () {

  },
  methods: {
    _getStudentList () {
      const req = {
        limit = 20,
        offset = 0
      }
      getStudentList(req).then((res) => {
        console.log(res)
      }).catch((res) => {
        console.error(res)
      })
    }
  }
}
复制代码

准备工做

1.拿到一份定义好的proto文件。

虽然语法简单,但其实前端不用怎么关心如何写proto文件,通常都是由后端来定义和维护。在这里你们能够直接用一下我定义好的一份demo

// User.proto
package framework;
syntax = "proto3";

message PBUser {
    uint64 user_id = 0;
    string name = 1;
    string mobile = 2;
}

// Class.proto
package school;
syntax = "proto3";

message PBClass {
    uint64 classId = 0;
    string name = 1;
}

// Student.proto
package school;
syntax = "proto3";

import "User.proto";
import "Class.proto";

message PBStudent {
    uint64 studentId = 0;
    PBUser user = 1;
    PBClass class = 2;
    PBStudentDegree degree = 3;
}

enum PBStudentDegree {
  PRIMARY = 0;   // 小学生
  MIDDLE = 1;    // 中学生
  SENIOR = 2;    // 高中生
  COLLEGE = 3;   // 大学生
}

message PBStudentListReq {
  uint32 offset = 1;
  uint32 limit = 2;
}

message PBStudentListRsp {
  repeated PBStudent list = 1;
}



// MessageType.proto
package framework;
syntax = "proto3";
// 公共请求体
message PBMessageRequest {
    uint32 type = 1;                            // 消息类型
    bytes messageData = 2;                      // 请求数据
    uint64 timestamp = 3;                       // 客户端时间戳
    string version = 4;                         // api版本号

    string token = 14;                          // 用户登陆后服务器返回的 token,用于登陆校验
}

// 消息响应包
message PBMessageResponse {
    uint32 type = 3;                            // 消息类型
    bytes messageData = 4;                      // 返回数据

    uint32 resultCode = 6;                      // 返回的结果码
    string resultInfo = 7;                      // 返回的结果消息提示文本(用于错误提示)
}
// 全部的接口
enum PBMessageType {
    // 学生相关
    getStudentList = 0;                         // 获取全部学生的列表, PBStudentListReq => PBStudentListRsp
}
复制代码

其实不用去学习proto的语法都能一目了然。这里有两种命名空间frameworkschoolPBStudent引用了PBUser,能够认为PBStudent继承了PBUser

通常来讲,先后端须要统一约束一个请求model和响应model,好比请求中哪些字段是必须的,返回体中又有哪些字段,这里用MessageType.protoPBMessageRequest来定义请求体所需字段,PBMessageResponse定义为返回体的字段。

PBMessageType 是接口的枚举,后端全部的接口都写在这里,用注释表示具体请求参数和返回参数类型。好比这里只定义了一个接口getStudentList

拿到后端提供的这份*.proto文件后,是否是已经能够基本了解到:有一个getStudentList的接口,请求参数是PBStudentListReq,返回的参数是PBStudentListRsp

因此说proto文件能够直接做为先后端沟通的文档。

步骤

1.新建一个vue项目

同时添加安装axiosprotobufjs

# vue create vue-protobuf
# npm install axios protobufjs --save-dev
复制代码

2.在src目录下新建一个proto目录,用来存放*.proto文件,并将写好的proto文件拷贝进去。

此时的项目目录和package.json

3.将*.proto文件生成src/proto/proto.js(重点)

protobufjs提供了一个叫pbjs的工具,这是一个神器,根据参数不一样能够打包成xx.json或xx.js文件。好比咱们想打包成json文件,在根目录运行:

npx pbjs -t json src/proto/*.proto > src/proto/proto.json
复制代码

能够在src/proto目录下生成一个proto.json文件,查看请点击这里。 以前说了:实践证实打包成js模块才是最好用的。我这里直接给出最终的命令

npx pbjs -t json-module -w commonjs -o src/proto/proto.js  src/proto/*.proto
复制代码

-w参数能够指定打包js的包装器,这里用的是commonjs,详情请各位本身去看文档。运行命令后在src/proto目录下生成的proto.js。在chrome中console.log(proto.js)一下:

能够发现,这个模块在原型链上定义了 load, lookup等很是有用的api,这正是后面咱们将会用到的。 为之后方便使用,咱们将命令添加到package.json的script中:

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "proto": "pbjs -t json-module -w commonjs -o src/proto/proto.js src/proto/*.proto"
  },
复制代码

之后更新proto文件后,只须要npm run proto便可从新生成最新的proto.js。

4. 封装request.js

在前面生成了proto.js文件后,就能够开始封装与后端交互的基础模块了。首先要知道,咱们这里是用axios来发起http请求的。

整个流程:开始调用接口 -> request.js将数据变成二进制 -> 前端真正发起请求 -> 后端返回二进制的数据 -> request.js处理二进制数据 -> 得到数据对象。

能够说request.js至关于一个加密解密的中转站。在src/lib目录下添加一个request.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'
})
复制代码

MessageType.proto里面定义了与后端约定的接口枚举、请求体、响应体。发起请求前须要将全部的请求转换为二进制,下面是request.js的主函数

import protoRoot from '@/proto/proto'
import protobuf from 'protobufjs'

// 请求体message
const PBMessageRequest = protoRoot.lookup('framework.PBMessageRequest')
// 响应体的message
const PBMessageResponse = protoRoot.lookup('framework.PBMessageResponse')

const apiVersion = '1.0.0'
const token = 'my_token'

function getMessageTypeValue(msgType) {
  const PBMessageType = protoRoot.lookup('framework.PBMessageType')
  const ret = PBMessageType.values[msgType]
  return ret
}

/**
 * 
 * @param {*} msgType 接口名称
 * @param {*} requestBody 请求体参数
 * @param {*} responseType 返回值
 */
function request(msgType, requestBody, responseType) { 
  // 获得api的枚举值
  const _msgType = getMessageTypeValue(msgType)

  // 请求须要的数据
  const reqData = {
    timeStamp: new Date().getTime(),
    type: _msgType,
    version: apiVersion,
    messageData: requestBody,
    token: token
  }
}
  // 将对象序列化成请求体实例
  const req = PBMessageRequest.create(reqData)
  
  // 调用axios发起请求
  // 这里用到axios的配置项:transformRequest和transformResponse
  // transformRequest 发起请求时,调用transformRequest方法,目的是将req转换成二进制
  // transformResponse 对返回的数据进行处理,目的是将二进制转换成真正的json数据
  return httpService.post('/api', req, {
    transformRequest,
    transformResponse: transformResponseFactory(responseType)
  }).then(({data, status}) => {
    // 对请求作处理
    if (status !== 200) {
      const err = new Error('服务器异常')
      throw err
    }
    console.log(data)
  },(err) => {
    throw err
  })
}
// 将请求数据encode成二进制,encode是proto.js提供的方法
function transformRequest(data) {
  return PBMessageRequest.encode(data).finish()
}

function isArrayBuffer (obj) {
  return Object.prototype.toString.call(obj) === '[object ArrayBuffer]'
}

function transformResponseFactory(responseType) {
  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.messageData && responseType) {
        const model = protoRoot.lookup(responseType)
        decodedResponse.messageData = model.decode(decodedResponse.messageData)
      }
      return decodedResponse
    } catch (err) {
      return err
    }
  }
}

// 在request下添加一个方法,方便用于处理请求参数
request.create = function (protoName, obj) {
  const pbConstruct = protoRoot.lookup(protoName)
  return pbConstruct.encode(obj).finish()
}

// 将模块暴露出去
export default request
复制代码

最后写好的具体代码请看:request.js。 其中用到了lookup(),encode(), finish(), decode()等几个proto.js提供的方法。

5. 调用request.js

在.vue文件直接调用api前,咱们通常不直接使用request.js来直接发起请求,而是将全部的接口再封装一层,由于直接使用request.js时要指定请求体,响应体等固定的值,屡次使用会形成代码冗余。

咱们习惯上在项目中将全部后端的接口放在src/api的目录下,如针对student的接口就放在src/api/student.js文件中,方便管理。 将getStudentList的接口写在src/api/student.js

import request from '@/lib/request'

// params是object类型的请求参数
// school.PBStudentListReq 是定义好的请求体model
// school.PBStudentListRsp 是定义好的响应model
// getStudentList 是接口名称
export function getStudentList (params) {
  const req = request.create('PBStudentListReq', params)
  return request('getStudentList', req, 'school.PBStudentListRsp')
}
// 后面若是再添加接口直接以此类推
export function getStudentById (id) {
  // const req = ...
  // return request(...)
}
复制代码

6. 在.vue中使用接口

须要哪一个接口,就import哪一个接口,返回的是Promise对象,很是方便。

<template>
  <div class="hello">
    <button @click="_getStudentList">获取学生列表</button>
  </div>
</template>

<script>
import { getStudentList } from '@/api/student'
export default {
  name: 'HelloWorld',
  methods: {
    _getStudentList () {
      const req = {
        limit: 20,
        offset: 0
      }
      getStudentList(req).then((res) => {
        console.log(res)
      }).catch((res) => {
        console.error(res)
      })
    }
  },
  created () {
  }
}
</script>

<style lang="scss">

</style>
复制代码

总结

整个demo的代码: demo

前端使用的整个流程:

  • 1. 将后端提供的全部的proto文件拷进src/proto文件夹
  • 2. 运行npm run proto 生成proto.js
  • 3. 根据接口枚举在src/api下写接口
  • 4. .vue文件中使用接口。

(其中1和2能够合并在一块儿写一个自动化的脚本,每次更新只需运行一下这个脚本便可)。

写的比较啰嗦,文笔也很差,你们见谅。

这个流程就是我感受比较好的一个proto在前端的实践,可能并非最好,若是在大家公司有其余更好的实践,欢迎你们一块儿交流分享。

后续

在vue中使用是须要打包成一个js模块来使用比较好(这是由于vue在生产环境中打包成只有html,css,js等文件)。但在某些场景,好比在Node环境中,一个Express的项目,生产环境中是容许出现.proto文件的,这时候能够采起protobuf.js提供的其余方法来动态解析proto,再也不须要npm run proto这种操做了。

后面有时间我会再写一篇在node端动态解析proto的记录。