一损合、 不修改源碼讓protobufjs適應(yīng)多平臺
我們上一篇《在cocos creator中使用protobufjs(一)》講解了通過修改源碼的方案,讓protobufjs能正常運(yùn)行在jsb環(huán)境上碍论。這個方案適合將protobufjs源碼直接放到項目中罐寨,而我們使用npm來管理三方庫的方式脱羡,這種方案就顯得不太優(yōu)雅捏膨。
1. 解決IS_NODE的檢查
之前源碼中已經(jīng)看到Util.IS_NODE是用來區(qū)分代碼是運(yùn)行在nodejs上還是瀏覽器上。我們可以模擬cocos-jsb為nodejs環(huán)境驯妄,我們看protobufjs是怎么來檢查環(huán)境的荷并。
Util.IS_NODE = !!(
typeof process === 'object' && process+'' === '[object process]' && !process['browser']
);
上面這段代碼我們注意兩個地方:
- !!:在一個變量或表達(dá)示前面使用“!!”的意思是將其值轉(zhuǎn)換為boolean值即true或false,這是js中常用的技術(shù)青扔,第一次見這種寫法的人可會犯暈源织。
- process:process對象是nodejs的內(nèi)置進(jìn)程對象,在cocos-jsb上肯定是沒這貨微猖,那怎么辦呢谈息?
方案一:偽裝者
在require('protobufjs')之前我們自己定義一個process對象
if(cc.sys.isNative) {
global.process = {
toString: () => '[object process]'
}
}
...
require('protobufjs');
這種方案相當(dāng)于欺騙protobufjs我們是nodejs,這段代碼也解釋兩句:
- global: global對象是js中很特殊的對象凛剥,全局的方法侠仇、屬性都集中在一個對象中。我們這里將process對象放到global上相當(dāng)于定義了全局變量犁珠。
- toString方法:js中所有對象上都具有toString方法(除null\undefined外)逻炊,當(dāng)你在對象上使用字符串連接“+”操作時,其實是調(diào)用的對象的toString方法犁享。
這種方法可將coco-jsb化身為nodejs嗅骄,但感覺有點(diǎn)文縐縐的,我們再看看更直接的方法饼疙。
方案二:霸王硬上弓

在require('protobufjs')之后強(qiáng)制修改Util.IS_NODE的值
protobufjs.Util.IS_NODE = cc.sys.isNative;
這個方法簡單直接溺森,而且不怕他修改檢查方案,我覺得這個方法更好窑眯。
2. 解決fs.readFile/fs.readFileSync
...
if (Util.IS_NODE) {
//cocos中那來的fs模塊呀屏积?
var fs = require("fs");
if (callback) {
fs.readFile(path, function(err, data) {
if (err)
callback(null);
else
callback(""+data);
});
} else
try {
return fs.readFileSync(path);
} catch (e) {
return null;
}
}
...
這里不能硬來了,硬來只能改源碼磅甩,使用偽裝的方法炊林,我們?nèi)ゾ帉懸粋€fs模塊
//fs.js
module.exports = {
//同步讀取文件
readFileSync(path) {
//cocos-jsb提供有相同功能的函數(shù),就借用下它
return jsb.fileUtils.getStringFromFile(path);
}
//異步讀取文件
readFile(path, cb) {
//cocos-jsb沒提供異步讀取文件的函數(shù)卷要,這里只能簡單執(zhí)行下回調(diào)傳回讀取內(nèi)容
let str = jsb.fileUtils.getStringFromFile(path);
cb(null, str);
},
}
我們這里是偷梁換柱渣聚,實現(xiàn)了一個fs模塊,這關(guān)算是過了僧叉。這里需要注意的是jsb.fileUtils對象奕枝,上面封裝有不少原生上的文件操作。
大多數(shù)方法一看名字就知道用法了瓶堕,這里就不再一一說明隘道。
3. 解決require("path")問題
源碼中有對path模塊的使用:
...
filename = require("path")['resolve'](filename);
...
fname = require("path")['join'](root, filename.file);
...
乍眼一看感覺這種寫法有點(diǎn)亂,其實它等同如下代碼:
let path = require("path");
filename = path.resolve(filename);
filename = path.join(root, filename.file);
這樣看就明白了,有個path模塊谭梗,調(diào)用了他的resolve和join方法忘晤,path偽裝再次登錄場:
//path.js
module.exports = {
//獲取全路徑
resolve: (subPath) => {
//使用cc.url.raw實現(xiàn)獲取全路徑
return cc.url.raw(`resources/${subPath}`);
},
// 方法使用平臺特定的分隔符把全部給定的path片段連接到一起
join: () => {
//使用cocos提供的cc.path.join實現(xiàn)
return cc.path.join.apply(null, arguments);
}
}
問題終于被被解決了,估計好多人會覺得好麻煩激捏!我的demo中已經(jīng)實現(xiàn)了這些偽裝者文件设塔。 寫這么多其實主要是想讓大家了解的是javascript語言的靈活性,以及一種思路一種可能性远舅。如果覺得還是不能接受壹置,下面我再給大家介紹一種方案,預(yù)編譯proto文件表谊。
二、 使用預(yù)編譯方案
在靜態(tài)語言中使用protobuf都需要將proto文件編譯成目標(biāo)代碼盖喷,protobufjs模塊也為我們提供了pbjs命令行工具爆办。
1. pbjs工具介紹
上圖是pbjs命令工具的幫助,看起來參數(shù)不少课梳,但我們這里只需要很簡單的使用距辆,生成json格式或js格式。
2. 將proto編譯為json
pbjs xxx.proto > xxx.json
無需任何選項暮刃,直接輸入文件名跨算,將輸出json格式的proto文件。
我們來看下如何使用:
let protobuf = require('protobufjs')
let builder = new protobuf.Builder();
protobuf.loadJsonFile('xxx.json', builder);
protobuf.loadJsonFile('yyy.json', builder);
let PB = builder.build('xxx.yyy.zzz');
其實使用json格式與使用proto格式?jīng)]什么大的差別椭懊。讀過源碼的話知道诸蚕,protobufjs庫加載proto文件的順序大致如下:
- 加載proto文件
- 將獲取的proto字符串,解析為json對象
- build操作將json對象轉(zhuǎn)換為proto對象
使用預(yù)編譯json加載相當(dāng)于省略了第二步氧猬,直接加載json文件轉(zhuǎn)換proto對象背犯。
當(dāng)proto文件比較多的時候,使用json加載可以提高一些效率盅抚。
3. 將proto編譯為js
pbjs -t commonjs xxx.proto > xxx.js
使用pbjs提供的-t參數(shù)將proto文件編譯為目標(biāo)格式漠魏,這里我們指定的commonjs,后面緊跟proto文件名妄均。
//-----------------------------proto文件內(nèi)容-----------------------------------
syntax = "proto3";
package grace.proto.msg;
message Player {
uint32 id = 1; //唯一ID 首次登錄時設(shè)置為0柱锹,由服務(wù)器分配
string name = 2; //顯示名字
uint64 enterTime = 3; //登錄時間
}
//-----------------------------編譯后的js文件內(nèi)容-------------------------------
module.exports = require("protobufjs").newBuilder({})['import']({
"package": "grace.proto.msg",
"syntax": "proto2",
"messages": [
{
"name": "Player",
"syntax": "proto3",
"fields": [
{
"rule": "optional",
"type": "uint32",
"name": "id",
"id": 1
},
{
"rule": "optional",
"type": "string",
"name": "name",
"id": 2
},
{
"rule": "optional",
"type": "uint64",
"name": "enterTime",
"id": 3
}
]
}
],
"isNamespace": true
}).build();
大致一看編譯后的js文件,其實與使用proto文件丰包、json文件加載沒什么本質(zhì)的區(qū)別禁熏,簡單分析下面代碼:
module.exports = require("protobufjs").newBuilder({})['import']({
//proto內(nèi)容的json格式
...
}).build();
1.require("protobufjs")導(dǎo)入protobufjs模塊,
2.newBuilder({}) 實例化一個builder對象
3.'import' 調(diào)用builder實例上的import方法導(dǎo)入一段json
4.build() 調(diào)用builder實例build方法邑彪,生成proto對象
5.module.exports 導(dǎo)出build()后的對象
使用預(yù)編譯js的方式不需要加載文件匹层,proto直接編寫在js文件中,當(dāng)proto文件較多時可以提高性能。
三升筏、 protobuf愛你不容易

我在使用protobuf的過程也不是一帆風(fēng)順撑柔,只能說protobuf愛你不容易!
1. 第一個項目
在最初的項目中您访,使用的是直接加載proto文件铅忿,當(dāng)時也沒想過使用預(yù)編譯的方式。項目中有接近上百個proto文件灵汪,proto文件由服務(wù)端程序定義的檀训,粒度非常小,幾個message就是一個proto文件享言。開發(fā)期間覺得沒什么問題峻凫,后來發(fā)布時,發(fā)現(xiàn)加載比較慢览露,性能差點(diǎn)的手機(jī)會特別明顯荧琼,因此還為加載proto文件的整個過程做了一個進(jìn)度條。
2. 卡牌項目
之后的一個卡牌項目中差牛,我們吸取了之前的經(jīng)驗命锄,與服務(wù)端程序討論定義proto文件時將同類數(shù)據(jù)結(jié)構(gòu)盡量定在一個文件中,不要太過分散偏化,任然使用直接加載proto文件的方式脐恩。在這項目中雖然protobuf的數(shù)據(jù)結(jié)構(gòu)更多,更復(fù)雜侦讨,但文件數(shù)量較少加載過程中沒有太大影響驶冒。
3. SLG項目
后來在一個SLG項目里我們?nèi)稳皇褂弥苯蛹虞dproto文件,但SLG項目的復(fù)雜度比之前的卡牌上升了好幾個數(shù)量級韵卤,protobuf文件個數(shù)只怎、數(shù)據(jù)結(jié)構(gòu)的規(guī)模都翻了幾倍,加載proto的加載過程在低配置手機(jī)上顯的非常慢怜俐,又只好為proto的加載過程制作進(jìn)度條身堡。
4. 小結(jié)
至此開始我才開始意識到直接加載大量proto文件的缺陷,在細(xì)讀protobufjs庫的文檔之后開始使用在項目中嘗試使用預(yù)編譯的方式拍鲤。
預(yù)編譯js方式解決了文件加載贴谎,但增加代碼編譯時間,在creator中可以將編譯的proto文件設(shè)置為插件季稳,不參與編譯擅这,但文件多了也是很麻煩。
預(yù)編譯json方式不會增加編譯時間景鼠,減少了proto到j(luò)son的轉(zhuǎn)換時間仲翎,但文件io操作任然是最大的瓶頸痹扇。
4. 覺知開發(fā)中的痛點(diǎn)
在protobuf的使用上,除了proto加載方案的選擇外溯香,還存在不少其它問題鲫构。
有項目使用json做協(xié)議,無需解碼玫坛,客戶端處理服務(wù)器響應(yīng)邏輯時比較方便结笨。但protobuf必須做解碼后才能讀取數(shù)據(jù)結(jié)構(gòu),proto對象的new湿镀、decode代碼充斥著客戶端項目炕吸。
在javascript項目使用protobuf還有一個痛點(diǎn)就是IDE無法很好支持proto對象的代碼補(bǔ)全,需要在代碼與proto原文件中來回切換勉痴,不時出現(xiàn)單詞拼寫錯誤等問題赫模。
下一次我們將繼續(xù)探索在項目中如何相對高效使用protobuf。