在 Cocos Creator 中使用 Protobufjs(一)

一. 環境準備

我一直在探索Cocos H5正確的開發姿式,目前作javascript項目已經離不開 nodejs、npm或grunt等腳手架工具了。javascript

1.初始化package.json文件

npm initjava

當新建好cocos-js或creator項目,在項目根目錄使用npm init命令,一路回車,將在當前目錄建立package.json文件用於nodejs三方模塊的管理。關於npm的使用細節網絡上有不少教程,在此不用細說。node

2. protobufjs模塊

file

本人最先在cocos2dx 2.x時代就開始用protobufjs模塊來操縱protobuf一直到如今。因此下面全部內容都是關於protobufjs在cocos creator中的使用,包括原平生臺(cocos2d-js也是大同小異)。android

安裝protobufjs到項目

npm install protobufjs@5 --saveios

使用npm install命令安裝模塊,注意咱們這裏使用的是protobufjs 5.x版本。 雖然protobufjs目前最新的 6.x版本,提供了ts、rpc等功能的支持,但有一個問題是在微信小遊戲中不能動態加載proto文件。c++

安裝protobufjs到全局

npm install -g protobufjs@5git

使用npm install -g 參數將模塊安裝到全局,目的主要是方便使用protobufjs提供的pbjs命令行工具。pbjs能夠將proto原文件轉換成json、js等,以提供不一樣的加載proto的方式,咱們能夠根據本身的實際狀況選擇使用。github

二. protobufjs用法

下面是demo中定義的Player.proto文件的內容web

syntax = "proto3";
package grace.proto.msg;

message Player {
    uint32  id = 1;         //惟一ID  首次登陸時設置爲0,由服務器分配
    string  name = 2;       //顯示名字
    uint64  enterTime = 3;  //登陸時間
}
複製代碼

關於proto具體語法細節這裏就很少說了,咱們重點如何將Player.proto文件中定義的Player對象在js中實例化、屬性賦值、序列化、反序列化操做。npm

1. 靜態語言中使用proto文件

在c++/java這類靜態語言中使用protobuf一般是使用官方提供的protoc命令將proto文件編譯成c++/java代碼,像下面這樣:

protoc --cpp_out=輸出路徑 xxx.proto protoc --java_out=輸出路徑 xxx.proto

將輸出路徑的文件導入對應語言的工程中使用。

2. 在creator項目中使用proto文件

file

javascript是動態語言,能夠在運行時產生對象,所以protobufjs提供了更爲便捷的動態編譯,將proto文件中的對象生成js對象,下面簡要講解一下在creator中具體的使用步驟:

1.加載proto文件並編譯生成proto對象

//導入protobufjs模塊
let protobuf = require("protobufjs");
//獲取一個builder對象
let builder = protobuf.newBuilder();
//使用protobufjs加文件,並與一個builder對象關聯
protobuf.protoFromFile('xxx.proto', builder);
protobuf.protoFromFile('yyy.proto', builder);
...
let PB = builder.build('grace.proto.msg'); 
複製代碼

這步操做主要是使用protobufjs加載、編譯proto文件。

2.實例化proto對象與屬性賦值

let PB = builder.build('grace.proto.msg')
複製代碼

build函數返回值PB對象中將包含的是在proto中定義全部message對象,如今已經成爲js對象,能夠被實例化,代碼以下:

//實例化Player
let player = new PB.Player();  
//屬性賦值
player.name = '張三';             
player.enterTime = Date.now();
複製代碼

3.proto對象的序列化與反序列化

不說廢話,仍是直接上代碼

...
//使用實例對象上的toArrayBuffer函數將對象序列化爲二進制數據
let data = player.toArrayBuffer();
//使用類型對象上的decode函數將二進制數據反序列化爲實例對象
let otherPlayer = PB.player.decode(data);
複製代碼

若是幸運你能夠在web上使用protobuf了, 爲何只是在web上呢,當你把上面的代碼運行在jsb環境下的時候,你會體驗到悲催的事情正在發生。

三. 拯救cocos-jsb上的protobufjs

爲何在原生上運行就掛掉了呢?要理解這個問題須要對nodejs\ 瀏覽器\cocos-jsb這三個javascript的運行宿主環境有必定的瞭解。

我以前的文章提到過在選擇nodejs模塊時,要注意是否同時支持nodejs和web,只要是純js的模塊在cocos中通常均可以隨便用,好比async、undersocre、lodash等。 protobufjs這個模塊是能夠很好的在瀏覽器和nodejs環境上運行的。但運行在cocos-jsb上就會出問題,首先咱們要定位到出問題的關鍵代碼:

protobuf.protoFromFile('xxx.proto', builder);
複製代碼

1. 問題分析

從protobuf.protoFromFile函數名上看就知道是要進行文件的加載,一想到文件加載,就涉及到文件操做的api,咱們來整理一下不一樣平臺上的文件接口:

宿主平臺 文件接口 說明
瀏覽器 XMLHttpRequest 瀏覽器中動態加載資源、文件等AJAX操做的基礎
nodejs fs.readFile / fs.readFileSync nodejs上的文件操做模塊,底層由c/c++實現
cocos-jsb jsb.fileUtils.getStringFromFile cocos-js提供的讀取文件內容接口,在不臺平臺(ios\android\windows)由不一樣底層api實現

看到這裏相信不少人已經明白爲何在cocos-jsb上會有問題了,咱們再來讀一下protobufjs源碼,證明下咱們的分析。

2. 分析protobufjs源碼

file
找到protobufjs加載文件的主要代碼,下面我爲源碼加上了註釋,請認真讀一下注釋內容:

Util.fetch = function(path, callback) {
    //檢查callback參數,callback參數決定是否爲異步加載
    if (callback && typeof callback != 'function')
        callback = null;

    //運行環境是否爲nodejs
    if (Util.IS_NODE) {
        //加載nodejs的文件系統模塊
        var fs = require("fs");  
        //檢查是否有callback,存在使用fs.readFile異步函數讀取文件內容
        if (callback) {
            fs.readFile(path, function(err, data) {
                if (err)
                    callback(null);
                else
                    callback(""+data);
            });
        } else
            //使用fs.readFileSync同步函數讀取文件內容 
            try {
                return fs.readFileSync(path);
            } catch (e) {
                return null;
            }
    } else {
        //當不爲nodejs運行環境使用XmlHttpRequest加載文件
        var xhr = Util.XHR();
        //根據callbcak參數是否存在,使用異步仍是同步方式
        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');
        //經過XmlHttpRequest.onreadystatechange事件函數異步獲取文件數據
        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;
            //調用send方法發起AJAX請求
            xhr.send(null);
        } else {
            ////調用send方法發起AJAX請求,同步獲取文件數據
            xhr.send(null);
            if (/* remote */ xhr.status == 200 || /* local */ (xhr.status == 0 && typeof xhr.responseText === 'string'))
                return xhr.responseText;
            return null;
        }
    }
};
複製代碼

從上面的代碼能夠看出protobufjs庫是爲瀏覽器和nodejs準備的,根本就沒考慮過cocos-jsb的存在(吐槽:建議cocos官方提供的接口能模仿nodejs這樣能少不少事),因此要在cocos-jsb中使用protobufjs***其中的一個辦法***就是修改protobufjs的源碼,以下:

Util.fetch = function(path, callback) {
    if (callback && typeof callback != 'function')
        callback = null;
    //將平臺檢查代碼改成cocos提供的接口
    if (cc.sys.isNative) {
        //文件讀取使用cocos-jsb提供的函數
        try {
            let data = jsb.fileUtils.getStringFromFile(path);
            cc.log(`proto文件內容: {data}`);
            return data;
        } catch (e) {
            return null;
        }
    } else {
        //web端無需修改,略
        ...
};
複製代碼

咱們用cocos的接口將代碼修改一下,加載問題就被化解了,問題真的被解決了嗎? 很差意思,除了上面要代碼外還有一處代碼須要修改,源碼以下:

BuilderPrototype["import"] = function(json, filename) {
    var delim = '/';

    // Make sure to skip duplicate imports

    if (typeof filename === 'string') {
        //這裏又出現了平臺檢查
        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繼續填坑

原本寫到這裏,問題大多已經解決了, 但此時,若是你滿懷信心地使用改造後的protobufjs源碼,將你的代碼運行起來那一刻,我相信絕大多數人會一臉蒙逼。

file

媽的根本就不行!!看了好多字,好不容易讀到這裏,不只在模擬器上跑不起來,在web上一樣也跑不起來。

怎麼辦,爲了完全解決問題,我還得繼續寫下去。

1. 瞭解creator動態加載資源的方法

請你們思考一個問題,creator項目中的一張圖片,在web與cocos-jsb上他們的文件路徑會同樣嗎?直接使用protobuf.protoFromFile('xxx.proto')去加載一個proto文件會成功嗎? cocos文檔中說過要動態加載一個圖片資源須要將文件存放在assets/resources目錄下,使用以下方法加載:

cc.loader.loadRes('resources/xxx')
複製代碼

嘗試將proto文件存放在resources/pb/目錄下,用使用如下代碼:

protobuf.protoFromFile('resources/pb/xxx.proto')
複製代碼

一樣會獲得失敗的提示,該如何辦呢?怎麼才能得到正確的資源路徑? 算了,不買關子了,寫累了直接出答案吧!

protobuf.protoFromFile(cc.url.raw('resources/pb/xxx.proto'));
複製代碼

cc.url.raw這個函數在瀏覽器、模擬器、手機上會返回不一樣的資源路徑,這纔是真正的資源路徑,這下代碼應該能夠正常運行起來了。

2. 更好的解決法辦

我一直在探索Cocos H5正確的開發方式,雖然經過修改protobufjs源碼的方法能夠來解決在cocos-jsb上運行的問題,但這並非惟一的解決方案。

如何在不修改protobufjs源碼的狀況下讓代碼運行起來,以及使用pbjs工具預編譯proto文件爲JSON和js文件的用法,請繼續觀注個人系列文章《當Creator趕上protobufjs》!

file
相關文章
相關標籤/搜索