在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)了問題:
- 路徑名、文件較長(zhǎng)容易寫錯(cuò)字徙菠。
- 項(xiàng)目開發(fā)中協(xié)議會(huì)不斷新增讯沈,會(huì)寫漏,少加載了proto文件懒豹。
- 某些原因會(huì)修改proto文件名芙盘,原來加載的沒及時(shí)修改,加載時(shí)會(huì)出錯(cuò)脸秽。
-
人工手寫這個(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();
- 在js中使用proto有個(gè)特點(diǎn)集绰,proto對(duì)象一般IDE都沒有代碼提示和著色规辱,在用調(diào)用proto對(duì)象解碼時(shí)輸入效率低下,還容易打錯(cuò)栽燕。
- 這句代碼暴露了協(xié)議細(xì)節(jié)罕袋,如果pb.LoginRep改名了也不知道改淑,代碼會(huì)報(bào)錯(cuò)。
- 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ì)省心很多藕赞。
三成肘、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.工廠模式
通信協(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ò)誤');
}
}
- cache是緩存net.send時(shí)的參數(shù)包括:action、sequence肛搬、callback没佑,其中sequence是自動(dòng)生成的并以它為key。
- 當(dāng)收到服務(wù)器數(shù)據(jù)時(shí)温赔,先解碼PBMessage蛤奢,用解碼后的sequence去查找出action。
- 使用action和data做為響應(yīng)工廠函數(shù)的參數(shù)陶贼,反序列化出響應(yīng)對(duì)象啤贩。
- 調(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í)厅贪。
解決辦法
要解決這個(gè)問題我目前的辦法是养涮,將proto對(duì)象生成對(duì)應(yīng)的js代碼,如果還想做的更好眉抬,可以學(xué)習(xí)Creator那樣贯吓,生成一個(gè)d.ts文件。
六蜀变、覺知你心中的痛
在開發(fā)中不能覺知到開發(fā)體驗(yàn)悄谐,估計(jì)也很難覺知到用戶體驗(yàn),因?yàn)樽约壕褪亲约喉?xiàng)目的用戶库北。不能覺知到痛爬舰,如何去解決痛?