前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Protocol Buffers 在前端项目中的使用

Protocol Buffers 在前端项目中的使用

作者头像
吴裕超
发布2018-08-01 11:35:49
6.2K0
发布2018-08-01 11:35:49
举报
文章被收录于专栏:吴裕超吴裕超

前言:

公司后端使用的是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来帮助我引入这些模块

代码语言:javascript
复制
var peopleMsg = require('./people_pb.js');
var message = new peopleMsg.peopleRequest(); // 创建一个MyMessage结构体

接下来设置参数,比如我们的.proto文件里有一个message数据结构

代码语言:javascript
复制
message Person {
  required string name = 1;
  required int32 id = 2;
  optional string phone_number= 3;
}

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

代码语言:javascript
复制
message.setName("John Doe");
message.setId(007);
message.setPhoneNumber(["800-555-1212"]);

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

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

代码语言:javascript
复制
var bytes = msg.serializeBinary()  //serializeBinary  序列化 

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

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

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

代码语言:javascript
复制
message PeopleInfo {
    string name= 1;
    uint32 age = 2;
    string city = 3; 
    string work_company = 4;
    bool isMarried = 5;
}

那么我们可以这么写

代码语言:javascript
复制
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");

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

代码语言:javascript
复制
// awesome.proto
package awesomepackage;
syntax = "proto3";

message AwesomeMessage {
    string awesome_field = 1; // becomes awesomeField
}
代码语言:javascript
复制
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,是的,我也是怎么干的,结果遇到坑了,我是这么写的

代码语言:javascript
复制
$.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的方法解析

代码语言:javascript
复制

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

第二次尝试

直接上代码

代码语言:javascript
复制
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的方法

代码语言:javascript
复制
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()

代码语言:javascript
复制
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不变

代码语言:javascript
复制
// 先使用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()  //序列化

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

代码语言:javascript
复制
var msg = new SharePB.SharePageResponse()
var res = SharePB.ShareResponse.deserializeBinary(response)
console.log('res', res)

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

查到官网issues 把

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

总结

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-07-19 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档