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结构体
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
deserializeBinary(response) 换成 deserializeBinary(new Uint8Array(response)
) 或 deserializeBinary(Array form(response)
) 后,依然报这个错,找了很多资料,还是没有找到解决方案,而且这个issues还是未关闭的,感觉google对js的pb库维护不太上心。
所以很尴尬,能上传数据,但是接收到的数据无法解析,最终我放弃了使用google官方的库,选择了protobuf.js
总结
这次采坑之路,足足花了我1个星期时间,英语本来就差的我,啃起文档来还是挺吃力的,之前也搜到了一些引用prototbuf.js在浏览器使用pb的博文,但是都比较粗糙,没有带来多少帮助,所以自己走通了之后写了下来,也想经过总结让自己对这块知识掌握更彻底,可能有很多纰漏,欢迎指正。
Protocol Buffers 在前端项目中的使用的更多相关文章
- 前端项目中使用jsencrypt进行字段加密
前端项目中使用jsencrypt进行字段加密. 使用步骤:①获取公钥②实例化对象③设置公钥④将所需数据进行加密然后返回. 进行一个简单的封装如下 /** * npm install jsencrypt ...
- 在Vue&Element前端项目中,使用FastReport + pdf.js生成并展示自定义报表
在我的<FastReport报表随笔>介绍过各种FastReport的报表设计和使用,FastReport报表可以弹性的独立设计格式,并可以在Asp.net网站上.Winform端上使用, ...
- 在Vue&Element前端项目中,对于字典列表的显示处理
在很多项目开发中,我们为了使用方便,一般都会封装一些自定义组件来简化界面的显示处理,例如参照字典的下拉列表显示,是我们项目中经常用到的功能之一,本篇随笔介绍在Vue&Element前端项目中如 ...
- 在Vue前端项目中,附件展示的自定义组件开发
在Vue前端界面中,自定义组件很重要,也很方便,我们一般是把一些通用的界面模块进行拆分,创建自己的自定义组件,这样操作可以大大降低页面的代码量,以及提高功能模块的开发效率,本篇随笔继续介绍在Vue&a ...
- 如何在前端项目中引用bootstrap less?
在基于bootstrap css框架的前端项目开发中,如果有grunt build系统,那么工作流是:客制化less,在less中定义自己的 CSS,同时可以随意引用bootstrap中预定义好的cs ...
- 前端项目中gulp的使用
在公司项目开发中,有一个前端项目,我们使用gulp来生成目标文件(css,js,html文件) 进入到这个项目目录中 C:\My Project\FrontEnd\TestBuilder 然后依次运 ...
- web前端项目中遇到的一些问题总结(08.23更新)
个人网站 https://iiter.cn 程序员导航站 开业啦,欢迎各位观众姥爷赏脸参观,如有意见或建议希望能够不吝赐教! 写一些最近工作中Vue项目中遇到的问题. 巴啦啦小魔仙,污卡拉,全身变,小 ...
- 前后端分离,如何在前端项目中动态插入后端API基地址?(in docker)
开门见山,本文分享前后端分离,容器化前端项目时动态插入后端API基地址,这是一个很赞的实践,解决了前端项目容器化过程中受制后端调用的尴尬. 尴尬从何而来 常见的web前后端分离:前后端分开部署,前端项 ...
- 前端项目中使用git来做分支和合并分支,管理生产版本
最近由于公司前端团队扩招,虽然小小的三四团队开发,但是也出现了好多问题.最让人揪心的是代码的管理问题:公司最近把版本控制工具从svn升级为git.前端H5组目前对git的使用还不是很熟悉,出现额多次覆 ...
随机推荐
- Android 之 Spinner 键值对的绑定(转)
很多时候我们会在下拉菜单中绑定一个值,但是 Spinner本身不提供这样的服务 于是在网上找了N久,终于找到一个简单易用的方案;废话不多说,直接上菜了 首先要定义一个Item类,有以下要注意的: ...
- modelsim10 SE 仿真lattice Xp2工程
1.首先要建立Lattice XP2库 在modelsim10 SE启动后.首先指定Lattice Diamond 1.4 给定的仿真器库源代码编译目录: C:\lscc\diamond\1.4\ca ...
- 我要AFO啦好伤感啊
我要AFO啦 虽然一直很垃圾 但是也很开心 接下来我要去学物理啦 原因是今天早上没有吃早餐?! 就这样把~ 白白
- 【BZOJ】2190 [SDOI2008]仪仗队
[算法]欧拉函数 欧拉线性筛 [题解]将图从左至右,从下至上,分别标号0~n-1. 除了坐标0,一个点会被观察到当且仅当其坐标(i,j)的i与j互质,否则会被(i/d,j/d)挡住. 所以累加2~n- ...
- 如何设计一个优雅健壮的Android WebView?(上)
转:如何设计一个优雅健壮的Android WebView?(上) 前言 Android应用层的开发有几大模块,其中WebView是最重要的模块之一.网上能够搜索到的WebView资料可谓寥寥,Gith ...
- idea如何搭建springmvc4
1.推荐大牛博客 此操作我操作了三次过后终于成功了,奉献大牛博客连接:做的非常详细到位,望各位采纳,推荐置顶. https://www.cnblogs.com/chenlinghong/p/83395 ...
- bzoj 3453 数论
首先我们知道对于f(x)来说,它是一个k次的多项式,那么f(x)的通项公式可以表示成一个k+1次的式子,且因为f(x)没有常数项,所以我们设这个式子为 f(x)=Σ(a[i]*x^i) (1<= ...
- c语言中的输入
先打个白条有时间在写 c语言中输入一行回车之后,以空格为单位进行的分割
- [Leetcode Week14]Maximum Binary Tree
Maximum Binary Tree 题解 原创文章,拒绝转载 题目来源:https://leetcode.com/problems/maximum-binary-tree/description/ ...
- C高级 跨平台协程库
1.0 协程库引言 协程对于上层语言还是比较常见的. 例如C# 中 yield retrun, lua 中 coroutine.yield 等来构建同步并发的程序. 本文就是探讨如何从底层实现开发级别 ...