一. 環(huán)境準(zhǔn)備
我一直在探索cocos H5正確的開(kāi)發(fā)姿勢(shì),目前做javascript項(xiàng)目已經(jīng)離不開(kāi) nodejs、npm或grunt等腳手架工具了。
1.初始化package.json文件
npm init
當(dāng)新建好cocos-js或creator項(xiàng)目,在項(xiàng)目根目錄使用npm init命令,一路回車(chē),將在當(dāng)前目錄創(chuàng)建package.json文件用于nodejs三方模塊的管理。關(guān)于npm的使用細(xì)節(jié)網(wǎng)絡(luò)上有很多教程,在此不用細(xì)說(shuō)。
2. protobufjs模塊
本人最早在cocos2dx 2.x時(shí)代就開(kāi)始用protobufjs模塊來(lái)操縱protobuf一直到現(xiàn)在。所以下面所有內(nèi)容都是關(guān)于protobufjs在cocos creator中的使用,包括原生平臺(tái)(cocos2d-js也是大同小異)。
安裝protobufjs到項(xiàng)目
npm install protobufjs@5 --save
使用npm install命令安裝模塊,注意我們這里使用的是protobufjs 5.x版本。 雖然protobufjs目前最新的 6.x版本,提供了ts、rpc等功能的支持,但接口變化太大,目前還不太會(huì)使用。
安裝protobufjs到全局
npm install -g protobufjs@5
使用npm install -g 參數(shù)將模塊安裝到全局,目的主要是方便使用protobufjs提供的pbjs命令行工具。pbjs可以將proto原文件轉(zhuǎn)換成json、js等,以提供不同的加載proto的方式,我們可以根據(jù)自己的實(shí)際情況選擇使用。
二. protobufjs用法
下面是demo中定義的Player.proto文件的內(nèi)容
syntax = "proto3";
package grace.proto.msg;
message Player {
uint32 id = 1; //唯一ID 首次登錄時(shí)設(shè)置為0,由服務(wù)器分配
string name = 2; //顯示名字
uint64 enterTime = 3; //登錄時(shí)間
}
關(guān)于proto具體語(yǔ)法細(xì)節(jié)這里就不多說(shuō)了,我們重點(diǎn)如何將Player.proto文件中定義的Player對(duì)象在js中實(shí)例化、屬性賦值、序列化、反序列化操作。
1. 靜態(tài)語(yǔ)言中使用proto文件
在c++/java這類(lèi)靜態(tài)語(yǔ)言中使用protobuf通常是使用官方提供的protoc命令將proto文件編譯成c++/java代碼,像下面這樣:
protoc --cpp_out=輸出路徑 xxx.proto
protoc --java_out=輸出路徑 xxx.proto
將輸出路徑的文件導(dǎo)入對(duì)應(yīng)語(yǔ)言的工程中使用。
2. 在creator項(xiàng)目中使用proto文件

javascript是動(dòng)態(tài)語(yǔ)言,可以在運(yùn)行時(shí)產(chǎn)生對(duì)象,因此protobufjs提供了更為便捷的動(dòng)態(tài)編譯,將proto文件中的對(duì)象生成js對(duì)象,下面簡(jiǎn)要講解一下在creator中具體的使用步驟:
1.加載proto文件并編譯生成proto對(duì)象
//導(dǎo)入protobufjs模塊
let protobuf = require("protobufjs");
//獲取一個(gè)builder對(duì)象
let builder = protobuf.newBuilder();
//使用protobufjs加文件,并與一個(gè)builder對(duì)象關(guān)聯(lián)
protobuf.protoFromFile('xxx.proto', builder);
protobuf.protoFromFile('yyy.proto', builder);
...
let PB = builder.build('grace.proto.msg');
這步操作主要是使用protobufjs加載、編譯proto文件。
2.實(shí)例化proto對(duì)象與屬性賦值
let PB = builder.build('grace.proto.msg')
build函數(shù)返回值PB對(duì)象中將包含的是在proto中定義所有message對(duì)象,現(xiàn)在已經(jīng)成為js對(duì)象,可以被實(shí)例化,代碼如下:
//實(shí)例化Player
let player = new PB.Player();
//屬性賦值
player.name = '張三';
player.enterTime = Date.now();
3.proto對(duì)象的序列化與反序列化
不說(shuō)廢話,還是直接上代碼
...
//使用實(shí)例對(duì)象上的toArrayBuffer函數(shù)將對(duì)象序列化為二進(jìn)制數(shù)據(jù)
let data = player.toArrayBuffer();
//使用類(lèi)型對(duì)象上的decode函數(shù)將二進(jìn)制數(shù)據(jù)反序列化為實(shí)例對(duì)象
let otherPlayer = PB.player.decode(data);
如果幸運(yùn)你可以在web上使用protobuf了, 為什么只是在web上呢,當(dāng)你把上面的代碼運(yùn)行在jsb環(huán)境下的時(shí)候,你會(huì)體驗(yàn)到悲催的事情正在發(fā)生。
三. 拯救cocos-jsb上的protobufjs
為什么在原生上運(yùn)行就掛掉了呢?要理解這個(gè)問(wèn)題需要對(duì)nodejs\ 瀏覽器\cocos-jsb這三個(gè)javascript的運(yùn)行宿主環(huán)境有一定的了解。
我之前的文章提到過(guò)在選擇nodejs模塊時(shí),要注意是否同時(shí)支持nodejs和web,只要是純js的模塊在cocos中一般都可以隨便用,比如async、undersocre、lodash等。
protobufjs這個(gè)模塊是可以很好的在瀏覽器和nodejs環(huán)境上運(yùn)行的。但運(yùn)行在cocos-jsb上就會(huì)出問(wèn)題,首先我們要定位到出問(wèn)題的關(guān)鍵代碼:
protobuf.protoFromFile('xxx.proto', builder);
1. 問(wèn)題分析
從protobuf.protoFromFile函數(shù)名上看就知道是要進(jìn)行文件的加載,一想到文件加載,就涉及到文件操作的api,我們來(lái)整理一下不同平臺(tái)上的文件接口:
| 宿主平臺(tái) | 文件接口 | 說(shuō)明 |
|---|---|---|
| 瀏覽器 | XMLHttpRequest | 瀏覽器中動(dòng)態(tài)加載資源、文件等AJAX操作的基礎(chǔ) |
| nodejs | fs.readFile / fs.readFileSync | nodejs上的文件操作模塊,底層由c/c++實(shí)現(xiàn) |
| cocos-jsb | jsb.fileUtils.getStringFromFile | cocos-js提供的讀取文件內(nèi)容接口,在不臺(tái)平臺(tái)(ios\android\windows)由不同底層api實(shí)現(xiàn) |
看到這里相信很多人已經(jīng)明白為什么在cocos-jsb上會(huì)有問(wèn)題了,我們?cè)賮?lái)讀一下protobufjs源碼,證實(shí)下我們的分析。
2. 分析protobufjs源碼

找到protobufjs加載文件的主要代碼,下面我為源碼加上了注釋?zhuān)?qǐng)認(rèn)真讀一下注釋內(nèi)容:
Util.fetch = function(path, callback) {
//檢查callback參數(shù),callback參數(shù)決定是否為異步加載
if (callback && typeof callback != 'function')
callback = null;
//運(yùn)行環(huán)境是否為nodejs
if (Util.IS_NODE) {
//加載nodejs的文件系統(tǒng)模塊
var fs = require("fs");
//檢查是否有callback,存在使用fs.readFile異步函數(shù)讀取文件內(nèi)容
if (callback) {
fs.readFile(path, function(err, data) {
if (err)
callback(null);
else
callback(""+data);
});
} else
//使用fs.readFileSync同步函數(shù)讀取文件內(nèi)容
try {
return fs.readFileSync(path);
} catch (e) {
return null;
}
} else {
//當(dāng)不為nodejs運(yùn)行環(huán)境使用XmlHttpRequest加載文件
var xhr = Util.XHR();
//根據(jù)callbcak參數(shù)是否存在,使用異步還是同步方式
xhr.open('GET', path, callback ? true : false);
// xhr.setRequestHeader('User-Agent', 'XMLHTTP/1.0');
xhr.setRequestHeader('Accept', 'text/plain');
if (typeof xhr.overrideMimeType === 'function') xhr.overrideMimeType('text/plain');
//通過(guò)XmlHttpRequest.onreadystatechange事件函數(shù)異步獲取文件數(shù)據(jù)
if (callback) {
xhr.onreadystatechange = function() {
if (xhr.readyState != 4) return;
if (/* remote */ xhr.status == 200 || /* local */ (xhr.status == 0 && typeof xhr.responseText === 'string'))
callback(xhr.responseText);
else
callback(null);
};
if (xhr.readyState == 4)
return;
//調(diào)用send方法發(fā)起AJAX請(qǐng)求
xhr.send(null);
} else {
////調(diào)用send方法發(fā)起AJAX請(qǐng)求,同步獲取文件數(shù)據(jù)
xhr.send(null);
if (/* remote */ xhr.status == 200 || /* local */ (xhr.status == 0 && typeof xhr.responseText === 'string'))
return xhr.responseText;
return null;
}
}
};
從上面的代碼可以看出protobufjs庫(kù)是為瀏覽器和nodejs準(zhǔn)備的,根本就沒(méi)考慮過(guò)cocos-jsb的存在(吐槽:建議cocos官方提供的接口能模仿nodejs這樣能少很多事),所以要在cocos-jsb中使用protobufjs其中的一個(gè)辦法就是修改protobufjs的源碼,如下:
Util.fetch = function(path, callback) {
if (callback && typeof callback != 'function')
callback = null;
//將平臺(tái)檢查代碼改為cocos提供的接口
if (cc.sys.isNative) {
//文件讀取使用cocos-jsb提供的函數(shù)
try {
let data = jsb.fileUtils.getStringFromFile(path);
cc.log(`proto文件內(nèi)容: {data}`);
return data;
} catch (e) {
return null;
}
} else {
//web端無(wú)需修改,略
...
};
我們用cocos的接口將代碼修改一下,加載問(wèn)題就被化解了,問(wèn)題真的被解決了嗎?
不好意思,除了上面要代碼外還有一處代碼需要修改,源碼如下:
BuilderPrototype["import"] = function(json, filename) {
var delim = '/';
// Make sure to skip duplicate imports
if (typeof filename === 'string') {
//這里又出現(xiàn)了平臺(tái)檢查
if (ProtoBuf.Util.IS_NODE)
// require("path")是加載nodejs的path模塊,resolve
filename = require("path")['resolve'](filename);
if (this.files[filename] === true)
return this.reset();
this.files[filename] = true;
} else if (typeof filename === 'object') { // Object with root, file.
var root = filename.root;
//這里還要修改
if (ProtoBuf.Util.IS_NODE)
root = require("path")['resolve'](root);
if (root.indexOf("\\") >= 0 || filename.file.indexOf("\\") >= 0)
delim = '\\';
var fname;
//這里還要修改
if (ProtoBuf.Util.IS_NODE)
fname = require("path")['join'](root, filename.file);
else
fname = root + delim + filename.file;
if (this.files[fname] === true)
return this.reset();
this.files[fname] = true;
}
...
}
這里我就不再貼修改代碼了,大家自行解決。
四 為protobuf繼續(xù)填坑
本來(lái)寫(xiě)到這里,問(wèn)題大多已經(jīng)解決了, 但此時(shí),如果你滿懷信心地使用改造后的protobufjs源碼,將你的代碼運(yùn)行起來(lái)那一刻,我相信絕大多數(shù)人會(huì)一臉蒙逼。
媽的根本就不行??!看了好多字,好不容易讀到這里,不僅在模擬器上跑不起來(lái),在web上同樣也跑不起來(lái)。
怎么辦,為了徹底解決問(wèn)題,我還得繼續(xù)寫(xiě)下去。
1. 了解creator動(dòng)態(tài)加載資源的方法
請(qǐng)大家思考一個(gè)問(wèn)題,creator項(xiàng)目中的一張圖片,在web與cocos-jsb上他們的文件路徑會(huì)一樣嗎?直接使用protobuf.protoFromFile('xxx.proto')去加載一個(gè)proto文件會(huì)成功嗎?
cocos文檔中說(shuō)過(guò)要?jiǎng)討B(tài)加載一個(gè)圖片資源需要將文件存放在assets/resources目錄下,使用如下方法加載:
cc.loader.loadRes('resources/xxx')
嘗試將proto文件存放在resources/pb/目錄下,用使用以下代碼:
protobuf.protoFromFile('resources/pb/xxx.proto')
同樣會(huì)得到失敗的提示,該如何辦呢?怎么才能獲得正確的資源路徑?
算了,不買(mǎi)關(guān)子了,寫(xiě)累了直接出答案吧!
protobuf.protoFromFile(cc.url.raw('resources/pb/xxx.proto'));
cc.url.raw這個(gè)函數(shù)在瀏覽器、模擬器、手機(jī)上會(huì)返回不同的資源路徑,這才是真正的資源路徑,這下代碼應(yīng)該可以正常運(yùn)行起來(lái)了。
2. 更好的解決法辦
我一直在探索cocos H5正確的開(kāi)發(fā)方式,雖然通過(guò)修改protobufjs源碼的方法可以來(lái)解決在cocos-jsb上運(yùn)行的問(wèn)題,但這并不是唯一的解決方案。
我這里編寫(xiě)了一個(gè)creator + protobufjs的demo沒(méi)有使用上述方案,地址如下:
如何在不修改protobufjs源碼的情況下讓代碼運(yùn)行起來(lái),以及使用pbjs工具預(yù)編譯proto文件為JSON和js文件的用法,請(qǐng)繼續(xù)觀注我的系列文章《探索cocosH5正確的開(kāi)發(fā)姿勢(shì)》!