Protocol Buffers,是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。
本文和大家聊聊怎样在前端使用 protobuf,在开始前先聊聊 JSON。
关于 JSON
我们前端通常是使用 JSON 格式作为数据格式,JSON也有优点:
1、原生支持
符合 JavaScript 原生语法,书写简单,直观一目了然
2、解析
我们可以使用 JSON.parse 和 JSON.stringify 来做反序列化和序列化,性能好,这一切不需要使用第三方库。
3、不需要描述文件
这点还是优于 PB 的,PB文件需要描述文件来做对应的解析,而 JSON 不需要描述文件。
4、抓包方便
我们可以很方便地通过 Chrome DevTools , Fiddler,Whistle 等抓包工具可以查看返回的 JSON 数据。
关于 protobuf
要使用 protobuf 首先要一个数据结构的描述文件,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。
优点:
1、体积小
protobuf 对数据序列化后,数据大小可约小3倍(当然,压缩比还要看具体内容)。
2、语言无关、平台无关
ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台。
3、高效
即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
4、扩展性、兼容性好
你可以更新数据结构,而不影响和破坏原有的旧程序。
为什么在前端使用 protobuf
protobuf 已经被后台广为使用,而且优点那么多,为什么前端不愿意使用呢?我认为很重要的两点:
1、不便于抓包和调试,因为 protobuf 需要依赖对应的描述文件才可以正确地把数据解析出来,而JSON不需要,这就导致在开发和测试场景下不便于抓包。
2、需要先把 protobuf 描述文件异步加载,这就多了一次网络请求,或者打包到业务代码中,导致业务代码变大。
3、解析后有很多冗余的字段,需要前端过滤和整理成可用的 JSON 结构体。
有了这些问题为什么还折腾 protobuf 呢?
其实在某些场景下比如单个请求数据量较大,上几M 的回包,当然也可以选择分包拉取,但这样无论并发加载还是阻塞加载,如果需要读取到某片数据,那么你的业务代码写起来将是将是非常复杂。
##数据对比
后端通过 protobuf 指令生成的 protobuf 描述文件,需要依赖于 google-protobuf.js。
拿我们的回放业务来做对比,课堂进入回放模式时需要加载互动字幕文件,如果这些数据使用 protobuf 会有什么变化?
先看看企鹅辅导的回放字幕文件结构:
注:因为数据有嵌套 protobuf ,所以要分为整体字幕文件 和 具体的交互 pb 描述文件,待解析到具体交互时,才会使用 交互 pb 描述文件去解析 。
文件名 | 说明 | 体积 |
---|---|---|
pbplaybackinfo_pb.js | 整体字幕 pb 描述文件 | 83KB |
chat_pb.js | 聊天 pb 描述文件 | 473KB |
envelope_pb.js | 红包 pb 描述文件 | 83KB |
exam_pb.js | 考试 pb 描述文件 | 35KB |
vote_pb.js | 投票 pb 描述文件 | 90KB |
注:以上 pb 描述文件整体 zip 之后 52 KB。
google-protobuf.js 大小:157 KB,zip 后 33KB。
一共 zip 后 85 KB (机子压缩工具没 gzip 格式,这里暂用 zip 。gzip 比 zip 压缩比更高)
(1)两种格式字幕文件体积对比
这里抽样对比pb格式的二进制字幕文件体积与对应的json格式的字幕文件的体积
pb字幕文件体积 | json字幕文件体积 | json体积/pb体积 |
---|---|---|
4.3M | 16M | 3.72 |
6.5K | 16K | 2.46 |
40K | 143K | 3.575 |
23K | 97K | 4.21 |
通过抽样对比可得,json格式的字幕文件的体积基本是pb格式的2倍以上,最多甚至是4倍以上,体积差异比较大。
(2)浏览器解析pb字幕文件速度对比
因为字幕文件中的信息节点较多,这里调研解析pb字幕文件的速度。使用的字幕文件是目前体积最大的文件,体积是4.3M。
解析字幕文件的过程分为两步:
解析外层数据,得到json格式概要信息 + 二进制具体数据
解析内层数据,得到json格式具体数据
字幕pb 文件(4.3M)做测试得出耗时数据:
解析步骤 | 耗时 |
---|---|
只解析外层数据 | 279ms |
解析外层数据+解析内层数据 | 1995ms |
总结:如果一次解析外层数据和内层数据,会耗时较长,而如果只解析内层数据只需要279ms,比较短。在实现的时候只需要解析外层数据得到概要信息(时间信息、消息类型等),在具体展示的时候才解析内层数据,这样是可以大幅降低初次解析数据导致的耗时。
兼容性
dcodeIO 给出的浏览器兼容 IE9+
Show you the code
后端提供给前端 pb 描述文件需要执行以下命令来生成 js 可用的 pb 描述文件
protoc --js_out=library=myproto_libs,binary:. messages.proto base.proto
假设前端已经拿到了 pb 描述文件 pbplaybackinfo_pb.js ,但描述文件依赖于 google-protobuf.js ,要整体打包后使用。
那么首先 npm i google-protobuf
在业务代码中开始引入:
const messages = require('./pbplaybackinfo_pb');
const playbackRspBody = new messages.PlaybackRspBody;
fetch('https://fudao.qq.com/pb/test.pb').then((res) => {
res.arrayBuffer().then((res) => {
console.time('pb');
const arrayBuffer = new Uint8Array(res);
console.log(messages.PlaybackRspBody.deserializeBinary(arrayBuffer, playbackRspBody).toObject());
console.timeEnd('pb');
});
});
接着我们这里的例子是异步拉取测试用的 pb 文件,假设是 test.pb。
因为 pb 数据文件是二进制的,所以这里需要使用 arrayBuffer() 的方式,待拉取完毕后再转 Uint8Array 类型。什么是 arrayBuffer ,什么是 uint8Array?
google-protobuf 提供的反序列化接口 deserializeBinary 必须是由对应的 pb 描述中的静态方提供。所以上面使用了 messages.PlaybackRspBody.deserializeBinary( )
到这里你拿到是未格式化的二进制 arrayBuffer,所以一定要 .toObject( ) ,官方文档没有提及到这个方法,在这我也折腾了比较长的时间。
最后
前端使用 json 还是 protobuf,还是看具体场景,普通的数据量小的接口当然是 JSON 最好,因为无论从可读性和调试成本上来看,这是最做优的。如果是数据量较大的情况下,需要综合业务代码的改造成本和用户体验等多方面做权衡。