當(dāng)creator遇上protobufjs—效率

在cocos creator中使用protobufjs(一)
在cocos creator中使用protobufjs(二)
通過前面兩篇我們探索了如何在creator中使用protobuf峰档,并且讓其能正常工作在瀏覽器赡勘、JSB上,最后聊到protobuf在js項(xiàng)目中使用上的一些痛點(diǎn)护戳。這篇博文我要把這些痛點(diǎn)一條一條地扳開,分析為什么它讓我痛,以及我的治療方案。

一甲脏、proto文件的加載問題

我遇到的第一個(gè)痛點(diǎn)就是proto文件的加載問題。有人可能會(huì)問妹笆,前面不是講了怎么加載方法很簡(jiǎn)單的:

...
let builder = new protobuf.Builder();
protobuf.loadProtoFile('aaa.proto', builder);
protobuf.loadProtoFile('bbb.proto', builder);
...

protobufjs是一個(gè)很優(yōu)秀的庫块请,他提供的loadProtoFile接口簡(jiǎn)單直接,但是在真實(shí)的項(xiàng)目開發(fā)中會(huì)像是上面這樣的嗎拳缠?proto文件是一開始就設(shè)計(jì)好了墩新,固定不變的嗎?文件名會(huì)修改嗎窟坐?文件會(huì)新增海渊、刪除嗎?

痛點(diǎn)分析

我只有第一天在cocos-js項(xiàng)目中使用proto時(shí)是將一個(gè)一個(gè)的proto文件名寫死在loadProtoFile的參數(shù)中的哲鸳,因?yàn)槟鞘俏抑型緟⑴c的項(xiàng)目臣疑,當(dāng)時(shí)我就發(fā)現(xiàn)了問題:

  1. 路徑名、文件較長(zhǎng)容易寫錯(cuò)字徙菠。
  2. 項(xiàng)目開發(fā)中協(xié)議會(huì)不斷新增讯沈,會(huì)寫漏,少加載了proto文件懒豹。
  3. 某些原因會(huì)修改proto文件名芙盘,原來加載的沒及時(shí)修改,加載時(shí)會(huì)出錯(cuò)脸秽。
  4. 人工手寫這個(gè)加載文件會(huì)很累,效率低下蝴乔,容易出錯(cuò)记餐,在文件眾多的情況下極度消耗腦細(xì)胞。


解決辦法

編寫代碼來生成代碼

我的解決辦法是編寫一個(gè)程序薇正,掃描proto文件目錄片酝,生成一個(gè)文件列表的數(shù)組,從而完全解放人工操作挖腰。

//protoFiles.js 用腳本自動(dòng)生成的文件
module.exports = [
  res/proto/aaa.proto,
  res/proto/bbb.proto,
  res/proto/zzz.proto,
  res/proto/login/xxx.proto
  ...
]

//pbhelper.js 編寫一個(gè)加載器
let protoFiles = require('protoFiles'); //導(dǎo)入自動(dòng)生成的proto文件列表
...
loadProtoFile() {
    let builder = new protobuf.Builder();
    //遍歷文件名雕沿,逐一加載
    protoFiles.forEach((protoFile) => {
        protobuf.loadProtoFile(protoFile, builder);
    })
    ...
}

從此再也不用擔(dān)心proto文件加載方面的問題了。

解放更多人工操作

在編寫proto掃描腳本的同時(shí)猴仑,還可以將proto文件同步到自己的工程目錄中审轮,以解決proto文件的手工復(fù)制粘貼問題,如果你還要更進(jìn)一步,還可以將svn/git的拉取給做了疾渣。
總結(jié)一下腳本要做的事:

1.從svn或git獲取最新的proto文件(svn: svn up, git: git pull origin master)
2.將proto文件同步到工程目錄
3.掃描工程目錄中的proto文件篡诽,生成一個(gè)文件列表數(shù)組

Creator中的新發(fā)現(xiàn)

最早在Creator中使用proto時(shí)我也是使用的上面的方法,但隨著對(duì)Creator的了解越來越多榴捡,我就在想杈女,Creator不是管理了我們所有的資源了嗎?cc.loader.loadResDir不是要以加載一個(gè)目錄下的所有資源吊圾,是否可以有更簡(jiǎn)單的辦法达椰?于是我嘗試著去調(diào)試loadResDir函數(shù)有驚喜發(fā)現(xiàn)。

let files = [];
//xxx是assets/resources目錄下的一個(gè)目錄名
cc.loader._resources.getUuidArray('xxx', null, files);
//files會(huì)得到所有的文件名
cc.log(files);

通過這個(gè)發(fā)現(xiàn)项乒,可以省去生成protoFiles.js的工作了啰劲。

二、proto對(duì)象的實(shí)例化問題

proto對(duì)象的實(shí)例化是一個(gè)痛點(diǎn)板丽,估計(jì)很多人會(huì)覺得有點(diǎn)小題大作呈枉。protobufjs不是提供了操作方法嗎,那么簡(jiǎn)單:

//實(shí)例化登錄請(qǐng)求
let loginReq = new pb.LoginRep();
loginReq.account = 'zxh';
loginReq.password = '123456';
//假如net是封裝好了的網(wǎng)絡(luò)模塊
net.send(pb.ActionCode.LOGIN, loginRsp, (data) => {
    //收到數(shù)據(jù)埃碱,反序列化
    let loginRsp = pb.LoginRsp.decode(data);
    ...
});

如果是做過網(wǎng)絡(luò)開發(fā)的應(yīng)該對(duì)上面的代碼不難理解猖辫,這里還是簡(jiǎn)單的解釋一下:

1.xxxRep是客戶端請(qǐng)求消息,xxxRsp 是服務(wù)器響應(yīng)消息砚殿,成對(duì)的設(shè)計(jì)請(qǐng)求啃憎、響應(yīng)協(xié)議比較好管理。
2.pb.ActionCode.LOGIN是一個(gè)常量定義似炎,是設(shè)計(jì)的請(qǐng)求操作碼辛萍,用于服務(wù)器識(shí)別你發(fā)的消息是登錄請(qǐng)求,而不是其它羡藐,不然序列化后的二進(jìn)制內(nèi)容服務(wù)器無法反序列化贩毕。
3.這里沒有出現(xiàn)客戶端proto對(duì)象的序列化操作,因?yàn)榭梢苑庋b到net.send函數(shù)中仆嗦,所以它不足以成為一個(gè)痛點(diǎn)辉阶。
4.net.send中的回調(diào)函數(shù)是客戶端響應(yīng)處理函數(shù),通過參數(shù)獲得服務(wù)器發(fā)送的數(shù)據(jù)瘩扼,因?yàn)槎M(jìn)制數(shù)據(jù)谆甜,所以需要用pb.LoginRsp.decode(data)進(jìn)行反序列化。

痛點(diǎn)分析

let loginReq = new pb.LoginRep();

  1. 在js中使用proto有個(gè)特點(diǎn)集绰,proto對(duì)象一般IDE都沒有代碼提示和著色规辱,在用調(diào)用proto對(duì)象解碼時(shí)輸入效率低下,還容易打錯(cuò)栽燕。
  2. 這句代碼暴露了協(xié)議細(xì)節(jié)罕袋,如果pb.LoginRep改名了也不知道改淑,代碼會(huì)報(bào)錯(cuò)。
  3. net.send(pb.ActionCode.LOGIN, loginReq, () => { }) 明明已經(jīng)是發(fā)送的登錄消息了炫贤,為什么還需要一個(gè)操作碼呢溅固?感覺有些累贅、重復(fù)兰珍。

解決辦法

工廠模式

如果能像下面一樣是不是會(huì)更清爽:

//使用工廠函數(shù)獲得LoginReq對(duì)象
let req = pb.newReq(pb.ActionCode.LOGIN);
req.account = 'zxh';
req.password = '123456';
//在工廠函數(shù)時(shí)做個(gè)小動(dòng)作:req.action = pb.ActionCode.LOGIN
//send時(shí)就不需要消息號(hào)參數(shù)了侍郭。
net.send(req, ...);

通過pb.newReq隱藏協(xié)議細(xì)節(jié),也不需要管消息的名字掠河,用的什么protobuf庫亮元,返回的req上綁定上action消息號(hào)減少調(diào)用send時(shí)的重復(fù)參數(shù),上層操作簡(jiǎn)單明了唠摹。
除了設(shè)計(jì)工廠函數(shù)外爆捞,還需要定義pb.ActionCode.LOGIN,讓它能被IDE自動(dòng)提示勾拉、代碼補(bǔ)全煮甥,文本著色,我們會(huì)省心很多藕赞。

利用工廠模式隱藏實(shí)現(xiàn)細(xì)節(jié)
利用工廠模式隱藏實(shí)現(xiàn)細(xì)節(jié)

三成肘、proto對(duì)象的反序列化問題

我們?cè)倏聪路葱蛄谢膱?chǎng)景

...
//發(fā)送數(shù)據(jù),net假如是封裝好了的網(wǎng)絡(luò)模塊
net.send(pb.ActionCode.LOGIN, loginReq, (data) => {
    //發(fā)送的是登錄請(qǐng)求斧蜕,反序列化時(shí)要用登錄響應(yīng)双霍,不然會(huì)失敗
    let loginRsp = pb.LoginRsp.decode(data);
    ...
});

痛點(diǎn)分析

反序列化成為痛點(diǎn)有部分原因與實(shí)例化相同,而且當(dāng)你收到一個(gè)響應(yīng)時(shí)批销,該用那個(gè)proto對(duì)象去反序列化會(huì)殺死不少腦細(xì)包洒闸,特別是在設(shè)計(jì)協(xié)議消息名字時(shí)不注意規(guī)范時(shí)更容易出錯(cuò)。

解決辦法

1.設(shè)計(jì)通信協(xié)議頭

2.請(qǐng)求\響應(yīng)唯一序列號(hào)
3.工廠模式

tcp協(xié)議頭

通信協(xié)議頭是客戶端均芽、服務(wù)器在收到二進(jìn)制數(shù)據(jù)時(shí)丘逸,可以使用一個(gè)固定的協(xié)議結(jié)構(gòu)去反序列也稱之為解碼。 解碼后可以獲得基本的數(shù)據(jù)掀宋,比如路由號(hào)鸣个、時(shí)間戳、用戶ID布朦、下層協(xié)議數(shù)據(jù)(二進(jìn)制)等,大概如下:

message PBMessage{
    int32 action = 1;     //消息號(hào)用于指明data字段(標(biāo)識(shí)下層協(xié)議類型)
    int32 sequence = 2;   //請(qǐng)求序列
    uint64 timestamp = 3; //時(shí)間戳
    int32 userID = 4;     //帳號(hào)
    bytes data = 5;       //請(qǐng)求或響應(yīng)數(shù)據(jù)(序列化后的二進(jìn)制數(shù)據(jù))
}

其中的sequence字段是客戶端向服務(wù)器發(fā)出一個(gè)請(qǐng)求時(shí)昼窗,生成的唯一ID是趴。當(dāng)服務(wù)器響應(yīng)你這個(gè)請(qǐng)求時(shí),傳回這個(gè)sequence澄惊,通過這個(gè)sequence + action你就能確定你的響應(yīng)消息對(duì)象唆途,從而正確解碼富雅。

//收到網(wǎng)絡(luò)數(shù)據(jù)
message(event) {
    var pbMessage = pb.PBMessage.decode(event.data);
    //從緩存對(duì)象中取出請(qǐng)求時(shí)的參數(shù)對(duì)象
    var obj = this.cache[pbMessage.sequence];
    //刪除緩存數(shù)據(jù)
    delete this.cache[pbMessage.sequence];
    try{
        //檢測(cè)緩存數(shù)據(jù)是否存在
        if (!obj) {
            return;
        }
        //使用工廠創(chuàng)建響應(yīng)對(duì)象
        let rsp = pb.newRsp(obj.action, obj.data);
        //調(diào)用請(qǐng)求時(shí)的回函數(shù) 
        obj.callback(rsp);
    }catch(e) {
      cc.log('處理響應(yīng)錯(cuò)誤');
    }        
}
  1. cache是緩存net.send時(shí)的參數(shù)包括:action、sequence肛搬、callback没佑,其中sequence是自動(dòng)生成的并以它為key。
  2. 當(dāng)收到服務(wù)器數(shù)據(jù)時(shí)温赔,先解碼PBMessage蛤奢,用解碼后的sequence去查找出action。
  3. 使用action和data做為響應(yīng)工廠函數(shù)的參數(shù)陶贼,反序列化出響應(yīng)對(duì)象啤贩。
  4. 調(diào)用響應(yīng)處理函數(shù)。

這時(shí)響應(yīng)函數(shù)就可以很輕松的處理業(yè)務(wù)了

//發(fā)送數(shù)據(jù)拜秧,net假如是封裝好了的網(wǎng)絡(luò)模塊
net.send(loginReq, (loginRsp) => {
    //直接訪問響應(yīng)對(duì)象痹屹,不需去解碼了
    this.label.string = loginRsp.player.name;
    ...
});

核心問題

不論是解決實(shí)例化還是反序列化,最核心的問題是實(shí)現(xiàn)那兩個(gè)工廠函數(shù)

let req = newReq(action);
let rsp = newRsp(action, data);

而實(shí)現(xiàn)這兩個(gè)工廠函數(shù)的前提是明確請(qǐng)求操作碼枉氮、請(qǐng)求對(duì)象志衍、響應(yīng)對(duì)象,需要建立一個(gè)映射表聊替,類似下面的定義

//proto中定義Action
enum ActionCode {
  LOGIN: 1,
  LOGOUT: 2,    
}

//protoMap.js文件
protoMap = {
    1: {
        req: pb.LoginRes,
        rsp: pb.LoginRsp,
    }
    ...
}

有了protoMap工廠函數(shù)就簡(jiǎn)單了

//工廠函數(shù)
let protoMap = require('protoMap');
//請(qǐng)求工廠函數(shù)
newReq(action) {
  let obj = protoMap[action];
  let req = new obj.req();
  req.action = action;
  return req;
}

//響應(yīng)工廠函數(shù)
newRsp(action, data) {
  let obj = protoMap[action];
  return obj.rsp.decode(data);
}

四楼肪、protoMap如何而來?

我們的問題是不是都解決呢佃牛?如果你覺得都解決了淹辞,那是高興的太早了。
目前protoMap.js文件是需要人手工去編寫的俘侠,同樣的問題又來了象缀。

痛點(diǎn)分析

1 一個(gè)項(xiàng)目與服務(wù)器的請(qǐng)求少則幾十個(gè),多則上百上千爷速,手工方式維護(hù)protoMap的難度大央星。
2.手工編寫這個(gè)protoMap.js文件在協(xié)議新增、修改惫东、刪除時(shí)容易出錯(cuò)莉给。
3.出了錯(cuò)問題還很不好找,只有在調(diào)用到的地方才能暴露問題廉沮。

解決辦法

編寫代碼來生成代碼

因?yàn)閜rotoMap.js是根據(jù)proto的定義動(dòng)態(tài)變化的颓遏,我采取的辦法是通過一個(gè)程序去分析proto文件生成protoMap代碼。不過這里為了讓protoMap生成器不要太復(fù)雜滞时,我在proto定義ActionCode時(shí)做了點(diǎn)小手腳

//proto中定義Action
enum ActionCode {
  LOGIN: 1,  //LoginReq;LoginRsp;
  LOGOUT: 2, //LogoutReq;LogoutRsp;   
}

在定義ActionCode時(shí)叁幢,我們?yōu)槊恳粋€(gè)消息碼加上注釋,第一個(gè)是請(qǐng)求坪稽,第二個(gè)是響應(yīng)曼玩。
如果在設(shè)計(jì)協(xié)議時(shí)鳞骤,能有嚴(yán)格的規(guī)范可以將注釋寫的簡(jiǎn)單些。

enum ActionCode {
  LOGIN: 1,  //Login
  LOGOUT: 2, //Logout
}

通過在ActionCode中加點(diǎn)小手腳黍判,再去解析這段文本豫尽,生成protoMap會(huì)簡(jiǎn)單很多了。在protoMap生成器中顷帖,可以去校驗(yàn)一下注釋中寫的請(qǐng)求美旧、響應(yīng)對(duì)象是否正確。

還有一種方案是在請(qǐng)求協(xié)議上添加注釋:

//action:1
message LoginReq {
    ...
}

//action:2
message LogoutReq {
  ...
}

這種方案我也在項(xiàng)目中使用過窟她,也可以方便提取生成protoMap陈症。

五、最后的痛

關(guān)于protobuf在js中還剩下最后一個(gè)痛震糖,那就是目前的IDE都不能支持proto對(duì)象屬性的

自動(dòng)補(bǔ)全录肯,代碼提示,文本著色

let req = pb.newReq(pb.ActionCode.LOGIN);
req.useName = 'zxh'; //這里應(yīng)該是userName被寫成useName
req.pwd = '123456';  //這里應(yīng)該是password被寫成pwd

痛點(diǎn)分析

1.js中沒有代碼提示容易筆誤吊说,而且問題大多數(shù)在運(yùn)行到代碼那一刻才會(huì)暴露出來论咏。
2.沒有自動(dòng)補(bǔ)全需要多打很多字。
3.沒有函數(shù)著色颁井,敲出來的代碼心里不踏實(shí)厅贪。

代碼提示、自動(dòng)補(bǔ)全可以提高開發(fā)效率雅宾、減少出錯(cuò)

解決辦法

要解決這個(gè)問題我目前的辦法是养涮,將proto對(duì)象生成對(duì)應(yīng)的js代碼,如果還想做的更好眉抬,可以學(xué)習(xí)Creator那樣贯吓,生成一個(gè)d.ts文件。

六蜀变、覺知你心中的痛

在開發(fā)中不能覺知到開發(fā)體驗(yàn)悄谐,估計(jì)也很難覺知到用戶體驗(yàn),因?yàn)樽约壕褪亲约喉?xiàng)目的用戶库北。不能覺知到痛爬舰,如何去解決痛?


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寒瓦,一起剝皮案震驚了整個(gè)濱河市情屹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杂腰,老刑警劉巖屁商,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蜡镶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門恤筛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來官还,“玉大人,你說我怎么就攤上這事毒坛⊥祝” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵煎殷,是天一觀的道長(zhǎng)屯伞。 經(jīng)常有香客問我,道長(zhǎng)豪直,這世上最難降的妖魔是什么劣摇? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮弓乙,結(jié)果婚禮上末融,老公的妹妹穿的比我還像新娘。我一直安慰自己暇韧,他們只是感情好勾习,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著懈玻,像睡著了一般巧婶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涂乌,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天艺栈,我揣著相機(jī)與錄音,去河邊找鬼骂倘。 笑死眼滤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的历涝。 我是一名探鬼主播诅需,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼荧库!你這毒婦竟也來了堰塌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤分衫,失蹤者是張志新(化名)和其女友劉穎场刑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚪战,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牵现,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年铐懊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞎疼。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡科乎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贼急,到底是詐尸還是另有隱情茅茂,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布太抓,位于F島的核電站空闲,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏走敌。R本人自食惡果不足惜碴倾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悔常。 院中可真熱鬧影斑,春花似錦、人聲如沸机打。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽残邀。三九已至皆辽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間芥挣,已是汗流浹背驱闷。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留空免,地道東北人空另。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蹋砚,于是被迫代替她去往敵國和親扼菠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理坝咐,服務(wù)發(fā)現(xiàn)循榆,斷路器,智...
    卡卡羅2017閱讀 134,651評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,072評(píng)論 25 707
  • 一墨坚、 不修改源碼讓protobufjs適應(yīng)多平臺(tái) 我們上一篇《在cocos creator中使用protobufj...
    張曉衡閱讀 6,198評(píng)論 3 8
  • 由于工程項(xiàng)目中擬采用一種簡(jiǎn)便高效的數(shù)據(jù)交換格式秧饮,百度了一下發(fā)現(xiàn)除了采用 xml、JSON 還有 ProtoBuf(...
    黃海佳閱讀 48,635評(píng)論 1 23
  • 當(dāng)孤單已經(jīng)變成一種習(xí)慣,習(xí)慣到我已經(jīng)不再去想該怎么辦盗尸,就算心煩意亂就算沒有人作伴柑船,自由和落寞之間怎么換算。 今天偶...
    八度黑白閱讀 618評(píng)論 19 20