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 反序列化

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

操作数据常用方法有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的方法解析

转换后的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的博文,但是都比较粗糙,没有带来多少帮助,所以自己走通了之后写了下来,也想经过总结让自己对这块知识掌握更彻底,可能有很多纰漏,欢迎指正。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏owent

boost.context-1.61版本的设计模型变化

之前写了个C++的协程框架libcopp,底层使用的是boost.context实现,然后剥离了对boost的依赖。然而这样意味着我必须时常跟进boost.co...

5981
来自专栏点滴积累

Fish Shell

今天看到阮一峰同学的一篇博客(Fish shell 入门教程),讲述的非常详细、清楚,有兴趣的可以直接转去查看此文,本文仅提供一下个人使用心得。 一、fish ...

3576
来自专栏iKcamp

手把手教你撸一个 Webpack Loader

文:小 boy(沪江网校Web前端工程师) 本文原创,转载请注明作者及出处 ? 经常逛 webpack 官网的同学应该会很眼熟上面的图。正如它宣传的一样,w...

4444
来自专栏java架构师

BAT美团滴滴java面试大纲(带答案版)之三:多线程synchronized

继续面试大纲系列文章。   从这一篇开始,我们进入ava编程中的一个重要领域---多线程!多线程就像武学中对的吸星大法,理解透了用好了可以得道成仙,俯瞰芸芸众生...

29810
来自专栏FreeBuf

渗透测试中利用基于时间差反馈的远程代码执行漏洞(Timed Based RCE)进行数据获取

在最近的渗透测试项目中,为了进一步验证漏洞的可用性和危害性,我们遇到了这样一种情形:构造基于时间差反馈的系统注入命令(OS command injection ...

2169
来自专栏cs

python经常用到的东西。

语法: 'sep'.join(seq) 参数说明 sep:分隔符。可以为空 seq:要连接的元素序列、字符串、元组、字典 上面的语法即:以sep作为分...

491
来自专栏Java Web

Java I/O不迷茫,一文为你导航!

学习过计算机相关课程的童鞋应该都知道,I/O 即输入Input/ 输出Output的缩写,最容易让人联想到的就是屏幕这样的输出设备以及键盘鼠标这一类的输入设备,...

1072
来自专栏有趣的django

35.Django2.0文档

第四章 模板  1.标签 (1)if/else {% if %} 标签检查(evaluate)一个变量,如果这个变量为真(即,变量存在,非空,不是布尔值假),系...

57010
来自专栏海天一树

小朋友学Python(1):Python简介与编程环境搭建

一、Python简介 不死Java,不朽C/C++,新贵Python。 Python(英国发音:/ˈpaɪθən/ 美国发音:/ˈpaɪθɑːn/), 是一种面...

33311
来自专栏码洞

依赖注入不是Java的专利,Golang也有

笔者在使用Golang的时候就发现构建系统依赖树非常繁琐,New了很多对象,又手工代码将它们拼接起来,写了一堆非常冗繁的代码。然后就开始想,要是Golang像J...

921

扫码关注云+社区