zoukankan      html  css  js  c++  java
  • Protocol Buffers 在前端项目中的使用

    前言:

    公司后端使用的是go语言,想尝试用pb和前端进行交互,于是便有了这一次尝试,共计花了一星期时间,网上能查到的文档几乎都看了一遍,但大多都是教在node环境下如何使用,普通的js环境下很多讲述的并不清楚,于是把自己的采坑之路总结一下,希望能让给大家提供一些参考。

    背景知识:

    还没听说过Protocol Buffers ? 传送门,简单的说,他和json、xml等类似,是一种数据结构,使用场景主要是作为一种数据传输格式来使用。它是二进制的,所以无论是发送请求还是接收请求都要用二进制格式,也就是说在给后端发送之前我们需要把传统的json数据转换为pb结构数据(二进制),接收后端传来的pb结构数据后,我们在使用之前要转为js里支持的常用数据类型,比如对象,数组,布尔等,有的pb结构数据类型js语言是没有的,这时候我们就要根据一些规则转为特定的数据类型。

    以往的工作流可能是

    前端和后端同时开发,简单的约定接口,然后前端根据约定的接口模拟数据,进行开发;

    或者更糟,

    前端后端分别开发,后端接口写好了前端再按后端定义的字段重新来一遍,

    会花费很多不必要的时间

    使用pb对接开发时,需要预先填写schema文件(即.proto),其实就是前后端一起定义一个.proto文件,接口名字,数据类型,字段,所有用到的都定义好,然后分别开发,没有特殊情况这个文件就不会再变动了,能提高一定效率(这是我在使用中的感受,至于pb本身相对于其他数据传输格式的优点,官网就有介绍,这里就不赘述了)

    所以使用pb之前,还需要了解一下pb的语法,因为要会写.proto文件啊,如果后端来写至少要能看懂才能用它工作啊,所以这个是一定要看的,也很简单,就是一种语法,现在的版本是proto3,以前的版本是proto2,略有不同,可以参考这篇文章

    探索之旅:

    好了,经过前期的学习,我们已经了解了pb是怎么回事,接下来我们要开始考虑如何使用pb通信了。经过调研,目前前端使用pb主要有两种方式,一个是google官方推出的protobuf for js,另一个是开源社区的protobuf.js。下面我分别介绍如何使用,本文我只介绍在浏览器环境下也就是一般开发情况下的使用教程,node环境下个人认为比浏览器坑要少得多,不再介绍,可以参考  安利贴:如何使用protobuf   在NodeJS中玩转Protocol Buffer

    一、google-protobuf

    官方的protobuf为各种主流语言都相关的库,js也不例外,但是文档却写的异常简单,让第一次接触pb的我着实懵逼了好一阵子,最后总结的步骤如下

    首先要安装protoc 编译器,

    https://github.com/google/protobuf/releases 下载protoc-3.6.0-win32.zip 和 protobuf-js-3.6.0.zip 就可以,不用管win32的字眼,64位系统亲测正常

    下载好解压后cd js && npm install 

    然后检查是否安装成功 protoc -v

    protoc在window上是一个cmd命令,他会把我们提前定义好的.proto文件转换为对应的js文件,

    $ protoc --js_out=library=myproto_libs,binary:. messages.proto base.proto
    $ protoc --js_out=import_style=commonjs,binary:. messages.proto base.proto

    如果你在文档上看到goog不知道它是怎么来的,可以了解一下google自己的JavaScript库:Closure。

    不同规范有不同的命令,这一部分可以参考官网,需要注意的是格式不要错

    生成对应的js文件之后,就可以在js中引入了,我是引入了require.js来帮助我引入这些模块

    var peopleMsg = require('./people_pb.js');
    var message = new peopleMsg.peopleRequest(); // 创建一个MyMessage结构体
    接下来设置参数,比如我们的.proto文件里有一个message数据结构
    message Person {
      required string name = 1;
      required int32 id = 2;
      optional string phone_number= 3;
    }

    现在我们可以给这个我们new的结构体添加信息

    message.setName("John Doe");
    message.setId(007);
    message.setPhoneNumber(["800-555-1212"]);

    注意,这里在proto文件里定义的字段为下划线分割的时候,set时必须变成大驼峰命名,phone_number => PhoneNumber; name => Name  这也是官网文档没有说明的地方。

    如果这时候后台需要我们传递这个massage

    var bytes = msg.serializeBinary()  //serializeBinary  序列化 

    这样就变成可以提交的参数啦!

    写数据搞定了,再说下读数据,也就是当我们接收到一个pb数据流,用google-protobuf怎么解析成我们想要的数据

    首先我们肯定知道返回数据的massage结构体,比如返回的结构体是这样的

    message PeopleInfo {
        string name= 1;
        uint32 age = 2;
        string city = 3; 
        string work_company = 4;
        bool isMarried = 5;
    }

    那么我们可以这么写

    var resMsg =PeopleMsg.PeopleResponse.deserializeBinary(res) //deserializeBinary 反序列化
    var name = resMsg.getName() 
    var city = resMsg.getCity()
    var work_company= resMsg.getWorkCompany()

    这样我们就可以读到服务器返回的信息了

    操作数据常用方法有4种

    setName() getName()  hasName()  clearName()  具体用法参考这里

    二、protobuf.js

    github和文档都介绍了browsers上怎么使用,但是给出的cdn实在不敢恭维,所以还是先下载到本地用script标签引用,或者require引入吧

    npm install protobufjs [--save --save-prefix=~]
    var protobuf = require("protobufjs");

    官方的文档很给力,直接拿过来吧

    // awesome.proto
    package awesomepackage;
    syntax = "proto3";
    
    message AwesomeMessage {
        string awesome_field = 1; // becomes awesomeField
    }
    protobuf.load("awesome.proto", function(err, root) {
        if (err)
            throw err;
    
        // Obtain a message type
        var AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage");
    
        // Exemplary payload
        var payload = { awesomeField: "AwesomeString" };
    
        // Verify the payload if necessary (i.e. when possibly incomplete or invalid)
        var errMsg = AwesomeMessage.verify(payload);
        if (errMsg)
            throw Error(errMsg);
    
        // Create a new message
        var message = AwesomeMessage.create(payload); // or use .fromObject if conversion is necessary
    
        // Encode a message to an Uint8Array (browser) or Buffer (node)
        var buffer = AwesomeMessage.encode(message).finish();
        // ... do something with buffer
    
        // Decode an Uint8Array (browser) or Buffer (node) to a message
        var message = AwesomeMessage.decode(buffer);
        // ... do something with message
    
        // If the application uses length-delimited buffers, there is also encodeDelimited and decodeDelimited.
    
        // Maybe convert the message back to a plain object
        var object = AwesomeMessage.toObject(message, {
            longs: String,
            enums: String,
            bytes: String,
            // see ConversionOptions
        });
    });

    分析代码可以知道,protobuf.js是直接引入.proto文件,然后按需获取massage对象,建立对应的json对象后转换为之前定义的massage格式对象,最后再转码为二进制,buffer即为可以传送给后台的对象了。可以发现比google官方的更清晰明了,先定义json再转换也非常方便易懂。

    这里需要注意的是,代码中payload定义json时,键名必须和massage里的对应,即这里的 awesome_field 和 awesomeField  ,massage里没有的这里定义了转化成buffer时buffer会成空的。 

     接收数据时,如果没有定义接收数据的massage类型需要先定义,然后再decode解码,解码之后是一个massag类型对象还不能直接使用,再使用toObject转为js的objec类型对象。然后上文中的object对象就可以正常使用了。

    protobuf.js的massage类型对象还有很多方法,可以去文档里查看。

    到了这里,我们了解了两个库的简单使用方法,应对一般的需求是够了,这时候你可能会觉得,很简单嘛,这有什么难的!确实,库和官网给的demo都很简单,但是当你实际使用的时候,才会发现到处都是坑啊,下面我们以一个需求为例,来一点点填坑,最终实现pb浏览器环境通信的正常使用。

    第一次尝试

    和node环境不一样,浏览器环境和服务器通信,我们要用ajax,这个时候,一般小型项目我们都会选择jquery,是的,我也是怎么干的,结果遇到坑了,我是这么写的

    $.ajax({
        url: 'xxx/xxx',
        type: 'post',
        dataType: 'text', 
        data: buffer, // 传入准备好的二进制数据
        processData: false, // 坑点 不写传给服务器的参数格式不对
        contentType: false, // 坑点 
        headers: {
          'Content-Type': 'application/protobuf' // 这里根据后台要求进行设置的,如果没有要求应该是 application/octet-stream (二进制流)
        },
        success: function (response) {
          console.log('Success:',response)
         var res = SharePB.ShareVideoBottomPageResponse.deserializeBinary(response)
        },
        error: function (err) {
          console.log('err',err)
        },
     })

    这里 processData  一定要设置,contentType,发送给服务器的编码类型,默认是application/x-www-form-urlencoded,经测试不设置依然能请求到pb数据,推荐设置,dataType是设置ajax的返回值类型: jquery只支持json, jsonp, script, xml, html, or text. 不支持blob或arrayBuffer,请求时会发现,数据是请求回来了,长这样

     

    先用protobuf.js的方法解析

    
    
    console.log('response', respoonse)

    var msg = resMessage.decode(new Uint8Array(response)) // resMassage 为提前创建的返回pb类型对象 response为ajax success函数返回值
    var resObj = resMessage.toObject(msg)
    console.log(
    'resObj', resObj)

    转换后的resObj是空的,实际上却是有值的,为什么呢,因为response不是二进制,不能直接被解析。那么jquery能解析二进制吗?到目前为止我没有找到答案,我查看了jquery的源码,里面没有对blob和arrayBuffer类型的支持,也没有相关方法。于是后来我放弃了jq,尝试用原生js去写。

    第二次尝试

    直接上代码

    function nativeXHR(postBuffer,resMessage) {
      var xhr = new XMLHttpRequest()
      xhr.open('post', url, true)
      xhr.responseType = 'arraybuffer' // 坑点!
      xhr.setRequestHeader('Content-Type', 'application/protobuf') //坑点!
      xhr.onload = function (response) {
        console.log('response', response)
        var msg = resMessage.decode(new Uint8Array(xhr.response)) // new Uint8Array() 坑点!
        console.log('msg', msg)
        var resObj = resMessage.toObject(msg)
        console.log('resObj', resObj)
      }
      xhr.send(postBuffer)
    }

    打印请求结果

    这里面有3个坑点

    第一个,xhr.responseType = 'arraybuffer',xhr.responseType必须设置为'arraybuffer',开始以为是被jquery阉割了,后来发现arraybuffer和blob是xhr 2 新增的,jquery刚出来的时候还没有,所以也就不说啥啦!

    第二个,xhr.setRequestHeader('Content-Type', 'application/protobuf'),其他格式都不可以,我不知道是后台设置的原因还是用pb必须这样,这个留着以后补充吧

    第三个,var msg = resMessage.decode(new Uint8Array(xhr.response)),这个是使用protobuf.js的一个坑,官方文档是写的是直接把数据放decode()里面就行,但是一运行就报错,后来翻阅到了这个库作者的wiki和 项目的issues以及MDN的一些写法,加上就能正常输出了。

    再试试fetch

    由于项目是移动端项目,所以不太用考虑兼容性,还是习惯用es6来写,于是又写了一个fetch的方法

    function jsFetch(postBuffer, resMessage) {
      fetch(url, {
          method: 'POST',
          headers: {
            "Content-Type": "application/protobuf"
          },
          body: postBuffer
        }).then(res => res.arrayBuffer()) // 坑点! arrayBuffer() 很关键,坑点,必须用arrayBuffer返回处理
        .catch(error => console.log('Error:', error))
        .then(response => {
        console.log('response',response)
    var msg = resMessage.decode(new Uint8Array(response)) var resObj = resMessage.toObject(msg) console.log('resObj', resObj) }, err => { console.log('err', err) }) }

    这里的坑是arrayBuffer(),一般情况下,第一个then里面都会写 res.json()

    fetch('http://example.com/movies.json')
      .then(function(response) {
        return response.json();
      })
      .then(function(myJson) {
        console.log(myJson);
      });

    而如果用pb传输的话,还写json,就会进入出错

    这时去查fetch的api,发现fetch的body有好几种方法,其中就有arrayBuffer() 设置之后,数据就能正常转换了。

    好,到这里,采用protobuf.js方案的ajax已经能够成功使用pb流了,接下来我们再试一下google-protobuf

    ajax不变

    // 先使用protoc 根据 share.proto 生成 share_pb.js
    var { SharePB } = require('./share_pb.js') // 引入生成的js文件
    ar msg = new SharePB.ShareVideoBottomPageRequest()
      msg.setVideoId('10976845541522') 
      msg.setTopicId('149137962904')
    var bytes = msg.serializeBinary()  //序列化

    经测试,生成的请求数据没问题,后台返回了二进制数据,下面是解析代码

    var msg = new SharePB.SharePageResponse()
    var res = SharePB.ShareResponse.deserializeBinary(response)
    console.log('res', res)

    经测试,报错,报错信息为:Type not convertible to Uint8Array

    查到官网issues 把

    deserializeBinary(response) 换成 deserializeBinary(new Uint8Array(response)) 或 deserializeBinary(Array form(response)) 后,依然报这个错,找了很多资料,还是没有找到解决方案,而且这个issues还是未关闭的,感觉google对js的pb库维护不太上心。
    所以很尴尬,能上传数据,但是接收到的数据无法解析,最终我放弃了使用google官方的库,选择了protobuf.js

    总结

    这次采坑之路,足足花了我1个星期时间,英语本来就差的我,啃起文档来还是挺吃力的,之前也搜到了一些引用prototbuf.js在浏览器使用pb的博文,但是都比较粗糙,没有带来多少帮助,所以自己走通了之后写了下来,也想经过总结让自己对这块知识掌握更彻底,可能有很多纰漏,欢迎指正。

  • 相关阅读:
    git
    avalonJS
    push
    DataTables使用学习记录
    django models使用学习记录
    js操作记录
    部署网站遇到的问题
    ubuntu修改文件权限记录
    django发送邮件
    ubuntu使用记录
  • 原文地址:https://www.cnblogs.com/wuyuchao/p/9229877.html
Copyright © 2011-2022 走看看