zoukankan      html  css  js  c++  java
  • # # # ProtoBuf

    Protobuf是一种轻便高效的结构化数据存储格式,官方定义平台无关、语言无关、可扩展、可用于通讯协议和数据存储等领域。

    它有以下优点:

    1、平台无关,语言无关,可扩展;

    2、他提供了友好的动态库,使用简单;

    3、解析速度快。比对应的XML快20-100倍;

    4、序列化数据非常简单、紧凑、与XHML相比、他的序列化之后的数据量约为1/3到1/10.

    ***前后端都可以直接在项目中使用protobuf,不用再特意额外定义model;

    ***protobuf可以直接作为前后端数据和接口的文档,大大减少了沟通成本;

    使用protobuf前后对比:

    前=>  后端语言定义的接口和字段,前端是不能直接使用的,前后端沟通是需要一份接口文档的,一旦后端字节有变化,需要修改文档并通知前端,文档更新不及时会造成遗漏加大沟通成本。

    后=>  protobuf文档是由后端统一定义的,可以直接作为文档,前端只需要将protobuf文件拷贝进前端项目即可,后端字段有改动,只需通知前端更新protobuf文件即可,因为后端是直接使用了protobuf文件,它一般是不会出现遗漏或错误的,这样的话团队合作的效率会增加。

    数据传递用json还是protobuf其实对我们开发人员来说没啥大区别,protobuf最后还是要解析成json才能用。

    我们公司的项目是基于vue开发的 同样这个也主要是说在vue中使用。

    我们要使用protobuf.js这个库来处理proto文件。

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

    ~ 直接解析,如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,然后再利用库提供的方法来解析数据,最后得到数据对象。
    实践中发现,转化成js文件会更好用一些,原因是转化后的js文件直接在原型链上定义了一些方法,非常方便。
    想在项目中封装一个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)
          })
        }
      }
    }

    当拿到一份定义好的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的语法。在这里有两种命名空间framework和school,PBStudent引用了PBUser,可以认为PBStudent继承了PBUser。

    一般来说,前后端需要统一约束一个请求model和响应model,比如请求中那些字段是必须的,返回体中又有那些字段,这里用MessageType.protoPBMessageRequest来定义请求体所需字段,PBMessageResponse定义为返回体的字段。

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

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

    说白了这个proto文件可以直接作为前后端沟通的文件。

    步骤

    新建一个vue项目

    同时添加安装axios和protobufjs。

    # vue create vue-protobuf
    # npm install axios protobufjs --save-dev
    

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

    现在的项目目录和package.json:

     

    将*.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文件。命令是:

    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)一下

    :

    这个模块在原型链上定义了loadlookup等非常有用的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。

    封装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

    在.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(...)
    }

     在.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>

    总结前端使用的整个流程:

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

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

    写在最后 
    在vue中使用是需要打包成一个js模块来使用比较好(这是因为vue在生产环境中打包成只有html,css,js等文件)。但在某些场景,比如在Node环境中,一个Express的项目,生产环境中是允许出现.proto文件的,这时候可以采取protobuf.js提供的其他方法来动态解析proto,不再需要npm run proto这种操作了。
                                                                                                  
     
     
                                                                                                                                                                                                      本文原文地址
                                                                                                                                                          https://juejin.im/post/5bcda388e51d457a1179da01
  • 相关阅读:
    409. Longest Palindrome(计算一组字符集合可以组成的回文字符串的最大长度)
    242. Valid Anagram(两个字符串包含的字符是否完全相同)
    17. Letter Combinations of a Phone Number(电话号码的字母组合)
    模块XML真垃圾
    数据库是什么
    python项目开发规范
    面向对象之类的成员
    面向对象
    模块之 import os 模块一
    模块之序列化 import json
  • 原文地址:https://www.cnblogs.com/jstll/p/10907053.html
Copyright © 2011-2022 走看看