因爲目前公司採用了ProtoBuf作先後端數據交互,進公司以來一直用的是公司大神寫好的基礎庫,徹底不瞭解底層是如何解析的,一旦報錯只能求人,做爲一隻還算有鑽研精神的猿,應該去了解一下底層的實現,在這裏記錄一下學習過程。css
Google Protocol Buffer(簡稱 Protobuf)是一種輕便高效的結構化數據存儲格式,平臺無關、語言無關、可擴展,可用於通信協議和數據存儲等領域。html
有幾個優勢:前端
- 1.平臺無關,語言無關,可擴展;
- 2.提供了友好的動態庫,使用簡單;
- 3.解析速度快,比對應的XML快約20-100倍;
- 4.序列化數據很是簡潔、緊湊,與XML相比,其序列化以後的數據量約爲1/3到1/10。
我的感覺: 先後端數據傳輸用json仍是protobuf其實對開發來講沒啥區別,protobuf最後仍是要解析成json才能用。我的以爲比較好的幾點是:vue
沒有使用protobuf以前,後端語言定義的接口和字段,前端是不能直接使用的,先後端溝通每每須要維護一份接口文檔,若是後端字段有改動,須要去修改文檔並通知前端,有時候文檔更新不及時或容易遺漏,溝通成本比較大。 使用protobuf後,protobuf文件由後端統必定義,protobuf直接能夠做爲文檔,前端只需將protobuf文件拷貝進前端項目便可。若是後端字段有改動,只需通知前端更新protobuf文件便可,由於後端是直接使用了protobuf文件,所以protobuf文件通常是不會出現遺漏或錯誤的。久而久之,團隊合做效率提高是明顯的。node
廢話了一大堆,下面進入正題。 我這裏講的主要是在vue中的使用,是目前本人所在的公司項目實踐,你們能夠當作參考。ios
前端中須要使用 protobuf.js 這個庫來處理proto文件。git
protobuf.js
提供了幾種方式來處理proto。github
protobuf.load("awesome.proto", function(err, root) {...})
protobuf.load("awesome.json", function(err, root) {...})
衆所周知,vue項目build後生成的dist目錄中只有html,css,js,images等資源,並不會有.proto
文件的存在,所以須要用protobuf.js
這個庫將*.proto
處理成*.js
或*.json
,而後再利用庫提供的方法來解析數據,最後獲得數據對象。chrome
PS: 實踐發現,轉化爲js文件會更好用一些,轉化後的js文件直接在原型鏈上定義了一些方法,很是方便。所以後面將會是使用這種方法來解析proto。vue-cli
在項目中封裝一個request.js
模塊,但願能像下面這樣使用,調用api時只需指定請求和響應的model,而後傳遞請求參數,不需關心底層是如何解析proto的,api返回一個Promise對象:
// /api/student.js 定義接口的文件
import request from '@/lib/request'
// params是object類型的請求參數
// school.PBStudentListReq 是定義好的請求體model
// school.PBStudentListRsp 是定義好的響應model
// getStudentList 是接口名稱
export function getStudentList (params) {
const req = request.create('school.PBStudentListReq', params)
return request('getStudentList', req, 'school.PBStudentListRsp')
}
// 在HelloWorld.vue中使用
import { getStudentList } from '@/api/student'
export default {
name: 'HelloWorld',
created () {
},
methods: {
_getStudentList () {
const req = {
limit = 20,
offset = 0
}
getStudentList(req).then((res) => {
console.log(res)
}).catch((res) => {
console.error(res)
})
}
}
}
複製代碼
雖然語法簡單,但其實前端不用怎麼關心如何寫proto文件,通常都是由後端來定義和維護。在這裏你們能夠直接用一下我定義好的一份demo。
// User.proto
package framework;
syntax = "proto3";
message PBUser {
uint64 user_id = 0;
string name = 1;
string mobile = 2;
}
// Class.proto
package school;
syntax = "proto3";
message PBClass {
uint64 classId = 0;
string name = 1;
}
// Student.proto
package school;
syntax = "proto3";
import "User.proto";
import "Class.proto";
message PBStudent {
uint64 studentId = 0;
PBUser user = 1;
PBClass class = 2;
PBStudentDegree degree = 3;
}
enum PBStudentDegree {
PRIMARY = 0; // 小學生
MIDDLE = 1; // 中學生
SENIOR = 2; // 高中生
COLLEGE = 3; // 大學生
}
message PBStudentListReq {
uint32 offset = 1;
uint32 limit = 2;
}
message PBStudentListRsp {
repeated PBStudent list = 1;
}
// MessageType.proto
package framework;
syntax = "proto3";
// 公共請求體
message PBMessageRequest {
uint32 type = 1; // 消息類型
bytes messageData = 2; // 請求數據
uint64 timestamp = 3; // 客戶端時間戳
string version = 4; // api版本號
string token = 14; // 用戶登陸後服務器返回的 token,用於登陸校驗
}
// 消息響應包
message PBMessageResponse {
uint32 type = 3; // 消息類型
bytes messageData = 4; // 返回數據
uint32 resultCode = 6; // 返回的結果碼
string resultInfo = 7; // 返回的結果消息提示文本(用於錯誤提示)
}
// 全部的接口
enum PBMessageType {
// 學生相關
getStudentList = 0; // 獲取全部學生的列表, PBStudentListReq => PBStudentListRsp
}
複製代碼
其實不用去學習proto的語法都能一目瞭然。這裏有兩種命名空間framework
和school
,PBStudent
引用了PBUser
,能夠認爲PBStudent
繼承了PBUser
。
通常來講,先後端須要統一約束一個請求model和響應model,好比請求中哪些字段是必須的,返回體中又有哪些字段,這裏用MessageType.proto
的PBMessageRequest
來定義請求體所需字段,PBMessageResponse
定義爲返回體的字段。
PBMessageType
是接口的枚舉,後端全部的接口都寫在這裏,用註釋表示具體請求參數和返回參數類型。好比這裏只定義了一個接口getStudentList
。
拿到後端提供的這份*.proto
文件後,是否是已經能夠基本瞭解到:有一個getStudentList
的接口,請求參數是PBStudentListReq
,返回的參數是PBStudentListRsp
。
因此說proto文件能夠直接做爲先後端溝通的文檔。
同時添加安裝axios
和protobufjs
。
# vue create vue-protobuf
# npm install axios protobufjs --save-dev
複製代碼
src
目錄下新建一個proto
目錄,用來存放*.proto
文件,並將寫好的proto文件拷貝進去。此時的項目目錄和package.json
:
*.proto
文件生成src/proto/proto.js
(重點)protobufjs
提供了一個叫pbjs的工具,這是一個神器,根據參數不一樣能夠打包成xx.json或xx.js文件。好比咱們想打包成json文件,在根目錄運行:
npx pbjs -t json src/proto/*.proto > src/proto/proto.json
複製代碼
能夠在src/proto
目錄下生成一個proto.json文件,查看請點擊這裏。 以前說了:實踐證實打包成js模塊纔是最好用的。我這裏直接給出最終的命令
npx pbjs -t json-module -w commonjs -o src/proto/proto.js src/proto/*.proto
複製代碼
-w
參數能夠指定打包js的包裝器,這裏用的是commonjs,詳情請各位本身去看文檔。運行命令後在src/proto目錄下生成的proto.js。在chrome中console.log(proto.js)
一下:
load
,
lookup
等很是有用的api,這正是後面咱們將會用到的。 爲之後方便使用,咱們將命令添加到package.json的script中:
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"proto": "pbjs -t json-module -w commonjs -o src/proto/proto.js src/proto/*.proto"
},
複製代碼
之後更新proto文件後,只須要npm run proto
便可從新生成最新的proto.js。
在前面生成了proto.js文件後,就能夠開始封裝與後端交互的基礎模塊了。首先要知道,咱們這裏是用axios來發起http請求的。
整個流程:開始調用接口 -> request.js將數據變成二進制 -> 前端真正發起請求 -> 後端返回二進制的數據 -> request.js處理二進制數據 -> 得到數據對象。
能夠說request.js至關於一個加密解密的中轉站。在src/lib
目錄下添加一個request.js
文件,開始開發:
既然咱們的接口都是二進制的數據,因此須要設置axios的請求頭,使用arraybuffer,以下:
import axios from 'axios'
const httpService = axios.create({
timeout: 45000,
method: 'post',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/octet-stream'
},
responseType: 'arraybuffer'
})
複製代碼
MessageType.proto
裏面定義了與後端約定的接口枚舉、請求體、響應體。發起請求前須要將全部的請求轉換爲二進制,下面是request.js的主函數
import protoRoot from '@/proto/proto'
import protobuf from 'protobufjs'
// 請求體message
const PBMessageRequest = protoRoot.lookup('framework.PBMessageRequest')
// 響應體的message
const PBMessageResponse = protoRoot.lookup('framework.PBMessageResponse')
const apiVersion = '1.0.0'
const token = 'my_token'
function getMessageTypeValue(msgType) {
const PBMessageType = protoRoot.lookup('framework.PBMessageType')
const ret = PBMessageType.values[msgType]
return ret
}
/**
*
* @param {*} msgType 接口名稱
* @param {*} requestBody 請求體參數
* @param {*} responseType 返回值
*/
function request(msgType, requestBody, responseType) {
// 獲得api的枚舉值
const _msgType = getMessageTypeValue(msgType)
// 請求須要的數據
const reqData = {
timeStamp: new Date().getTime(),
type: _msgType,
version: apiVersion,
messageData: requestBody,
token: token
}
}
// 將對象序列化成請求體實例
const req = PBMessageRequest.create(reqData)
// 調用axios發起請求
// 這裏用到axios的配置項:transformRequest和transformResponse
// transformRequest 發起請求時,調用transformRequest方法,目的是將req轉換成二進制
// transformResponse 對返回的數據進行處理,目的是將二進制轉換成真正的json數據
return httpService.post('/api', req, {
transformRequest,
transformResponse: transformResponseFactory(responseType)
}).then(({data, status}) => {
// 對請求作處理
if (status !== 200) {
const err = new Error('服務器異常')
throw err
}
console.log(data)
},(err) => {
throw err
})
}
// 將請求數據encode成二進制,encode是proto.js提供的方法
function transformRequest(data) {
return PBMessageRequest.encode(data).finish()
}
function isArrayBuffer (obj) {
return Object.prototype.toString.call(obj) === '[object ArrayBuffer]'
}
function transformResponseFactory(responseType) {
return function transformResponse(rawResponse) {
// 判斷response是不是arrayBuffer
if (rawResponse == null || !isArrayBuffer(rawResponse)) {
return rawResponse
}
try {
const buf = protobuf.util.newBuffer(rawResponse)
// decode響應體
const decodedResponse = PBMessageResponse.decode(buf)
if (decodedResponse.messageData && responseType) {
const model = protoRoot.lookup(responseType)
decodedResponse.messageData = model.decode(decodedResponse.messageData)
}
return decodedResponse
} catch (err) {
return err
}
}
}
// 在request下添加一個方法,方便用於處理請求參數
request.create = function (protoName, obj) {
const pbConstruct = protoRoot.lookup(protoName)
return pbConstruct.encode(obj).finish()
}
// 將模塊暴露出去
export default request
複製代碼
最後寫好的具體代碼請看:request.js。 其中用到了lookup()
,encode()
, finish()
, decode()
等幾個proto.js提供的方法。
在.vue文件直接調用api前,咱們通常不直接使用request.js來直接發起請求,而是將全部的接口再封裝一層,由於直接使用request.js時要指定請求體,響應體等固定的值,屢次使用會形成代碼冗餘。
咱們習慣上在項目中將全部後端的接口放在src/api
的目錄下,如針對student的接口就放在src/api/student.js
文件中,方便管理。 將getStudentList
的接口寫在src/api/student.js
中
import request from '@/lib/request'
// params是object類型的請求參數
// school.PBStudentListReq 是定義好的請求體model
// school.PBStudentListRsp 是定義好的響應model
// getStudentList 是接口名稱
export function getStudentList (params) {
const req = request.create('PBStudentListReq', params)
return request('getStudentList', req, 'school.PBStudentListRsp')
}
// 後面若是再添加接口直接以此類推
export function getStudentById (id) {
// const req = ...
// return request(...)
}
複製代碼
須要哪一個接口,就import哪一個接口,返回的是Promise對象,很是方便。
<template>
<div class="hello">
<button @click="_getStudentList">獲取學生列表</button>
</div>
</template>
<script>
import { getStudentList } from '@/api/student'
export default {
name: 'HelloWorld',
methods: {
_getStudentList () {
const req = {
limit: 20,
offset: 0
}
getStudentList(req).then((res) => {
console.log(res)
}).catch((res) => {
console.error(res)
})
}
},
created () {
}
}
</script>
<style lang="scss">
</style>
複製代碼
整個demo的代碼: demo。
前端使用的整個流程:
src/proto
文件夾npm run proto
生成proto.jssrc/api
下寫接口.vue
文件中使用接口。(其中1和2能夠合併在一塊兒寫一個自動化的腳本,每次更新只需運行一下這個腳本便可)。
寫的比較囉嗦,文筆也很差,你們見諒。
這個流程就是我感受比較好的一個proto在前端的實踐,可能並非最好,若是在大家公司有其餘更好的實踐,歡迎你們一塊兒交流分享。
在vue中使用是須要打包成一個js模塊來使用比較好(這是由於vue在生產環境中打包成只有html,css,js等文件)。但在某些場景,好比在Node環境中,一個Express的項目,生產環境中是容許出現.proto
文件的,這時候能夠採起protobuf.js
提供的其餘方法來動態解析proto,再也不須要npm run proto這種操做了。
後面有時間我會再寫一篇在node端動態解析proto的記錄。