前言:
公司后端使用的是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的博文,但是都比較粗糙,沒有帶來多少幫助,所以自己走通了之后寫了下來,也想經過總結讓自己對這塊知識掌握更徹底,可能有很多紕漏,歡迎指正。