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

查到官網issues 把

deserializeBinary(response) 換成 deserializeBinary(new Uint8Array(response)) 或 deserializeBinary(Array form(response)) 后,依然報這個錯,找了很多資料,還是沒有找到解決方案,而且這個issues還是未關閉的,感覺google對js的pb庫維護不太上心。
所以很尷尬,能上傳數據,但是接收到的數據無法解析,最終我放棄了使用google官方的庫,選擇了protobuf.js

總結

這次采坑之路,足足花了我1個星期時間,英語本來就差的我,啃起文檔來還是挺吃力的,之前也搜到了一些引用prototbuf.js在瀏覽器使用pb的博文,但是都比較粗糙,沒有帶來多少幫助,所以自己走通了之后寫了下來,也想經過總結讓自己對這塊知識掌握更徹底,可能有很多紕漏,歡迎指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM