一冯勉,添加熱更新需要的文件
1. 在項目根目錄添加 version_generator.js 文件
? version_generator.js 內(nèi)容如下:
/**
* 此模塊用于熱更新工程清單文件的生成
*/
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var manifest = {
//服務(wù)器上資源文件存放路徑(src,res的路徑)
packageUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/',
//服務(wù)器上project.manifest路徑
remoteManifestUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/project.manifest',
//服務(wù)器上version.manifest路徑
remoteVersionUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/version.manifest',
version: '1.0.0',
assets: {},
searchPaths: []
};
//生成的manifest文件存放目錄
var dest = 'assets/';
//項目構(gòu)建后資源的目錄
var src = 'build/jsb-link/';
/**
* node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/
*/
// Parse arguments
var i = 2;
while ( i < process.argv.length) {
var arg = process.argv[i];
switch (arg) {
case '--url' :
case '-u' :
var url = process.argv[i+1];
manifest.packageUrl = url;
manifest.remoteManifestUrl = url + 'project.manifest';
manifest.remoteVersionUrl = url + 'version.manifest';
i += 2;
break;
case '--version' :
case '-v' :
manifest.version = process.argv[i+1];
i += 2;
break;
case '--src' :
case '-s' :
src = process.argv[i+1];
i += 2;
break;
case '--dest' :
case '-d' :
dest = process.argv[i+1];
i += 2;
break;
default :
i++;
break;
}
}
function readDir (dir, obj) {
var stat = fs.statSync(dir);
if (!stat.isDirectory()) {
return;
}
var subpaths = fs.readdirSync(dir), subpath, size, md5, compressed, relative;
for (var i = 0; i < subpaths.length; ++i) {
if (subpaths[i][0] === '.') {
continue;
}
subpath = path.join(dir, subpaths[i]);
stat = fs.statSync(subpath);
if (stat.isDirectory()) {
readDir(subpath, obj);
}
else if (stat.isFile()) {
// Size in Bytes
size = stat['size'];
md5 = crypto.createHash('md5').update(fs.readFileSync(subpath, 'binary')).digest('hex');
compressed = path.extname(subpath).toLowerCase() === '.zip';
relative = path.relative(src, subpath);
relative = relative.replace(/\\/g, '/');
relative = encodeURI(relative);
obj[relative] = {
'size' : size,
'md5' : md5
};
if (compressed) {
obj[relative].compressed = true;
}
}
}
}
var mkdirSync = function (path) {
try {
fs.mkdirSync(path);
} catch(e) {
if ( e.code != 'EEXIST' ) throw e;
}
}
// Iterate res and src folder
readDir(path.join(src, 'src'), manifest.assets);
readDir(path.join(src, 'res'), manifest.assets);
var destManifest = path.join(dest, 'project.manifest');
var destVersion = path.join(dest, 'version.manifest');
mkdirSync(dest);
fs.writeFile(destManifest, JSON.stringify(manifest), (err) => {
if (err) throw err;
console.log('Manifest successfully generated');
});
delete manifest.assets;
delete manifest.searchPaths;
fs.writeFile(destVersion, JSON.stringify(manifest), (err) => {
if (err) throw err;
console.log('Version successfully generated');
});
? 注意:以下幾個地方摔桦,你可能需要根據(jù)自己的需求修改调缨,本文 第三節(jié)第一點(diǎn) 會指出各個參數(shù)對應(yīng)的情況丧肴。
2. 添加熱更新組件穿挨,并掛在熱更新腳本
這里我簡單新建了一個helloWorld工程
添加了兩個button月弛,check用于檢測是否有更新,update用于更新資源科盛;
在canvas上掛載HotUpdate腳本帽衙,注意:這時ManifestUrl為空。
給button check添加點(diǎn)擊事件贞绵,綁定checkForUpdate()
給button update添加點(diǎn)擊事件厉萝,綁定hotUpdate()
HotUpdate.js
/**
* 負(fù)責(zé)熱更新邏輯的組件
*/
cc.Class({
extends: cc.Component,
properties: {
manifestUrl: cc.RawAsset, //本地project.manifest資源清單文件
_updating: false,
_canRetry: false,
_storagePath: ''
},
checkCb: function (event) {
cc.log('Code: ' + event.getEventCode());
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
cc.log("No local manifest file found, hot update skipped.");
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log("Fail to download manifest file, hot update skipped.");
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log("Already up to date with the latest remote version.");
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
cc.log('New version found, please try to update.');
this.hotUpdate();
break;
default:
return;
}
cc.eventManager.removeListener(this._checkListener);
this._checkListener = null;
this._updating = false;
},
updateCb: function (event) {
var needRestart = false;
var failed = false;
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
cc.log('No local manifest file found, hot update skipped...');
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
cc.log(event.getPercent());
cc.log(event.getPercentByFile());
cc.log(event.getDownloadedFiles() + ' / ' + event.getTotalFiles());
cc.log(event.getDownloadedBytes() + ' / ' + event.getTotalBytes());
var msg = event.getMessage();
if (msg) {
cc.log('Updated file: ' + msg);
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log('Fail to download manifest file, hot update skipped.');
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log('Already up to date with the latest remote version.');
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
cc.log('Update finished. ' + event.getMessage());
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
cc.log('Update failed. ' + event.getMessage());
this._updating = false;
this._canRetry = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
cc.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
cc.log(event.getMessage());
break;
default:
break;
}
if (failed) {
cc.eventManager.removeListener(this._updateListener);
this._updateListener = null;
this._updating = false;
}
if (needRestart) {
cc.eventManager.removeListener(this._updateListener);
this._updateListener = null;
// Prepend the manifest's search path
var searchPaths = jsb.fileUtils.getSearchPaths();
var newPaths = this._am.getLocalManifest().getSearchPaths();
cc.log(JSON.stringify(newPaths));
Array.prototype.unshift(searchPaths, newPaths);
// This value will be retrieved and appended to the default search path during game startup,
// please refer to samples/js-tests/main.js for detailed usage.
// !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
cc.sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
jsb.fileUtils.setSearchPaths(searchPaths);
cc.audioEngine.stopAll();
cc.game.restart();
}
},
retry: function () {
if (!this._updating && this._canRetry) {
this._canRetry = false;
cc.log('Retry failed Assets...');
this._am.downloadFailedAssets();
}
},
checkForUpdate: function () {
cc.log("start checking...");
if (this._updating) {
cc.log('Checking or updating ...');
return;
}
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
this._am.loadLocalManifest(this.manifestUrl);
cc.log(this.manifestUrl);
}
if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
cc.log('Failed to load local manifest ...');
return;
}
this._checkListener = new jsb.EventListenerAssetsManager(this._am, this.checkCb.bind(this));
cc.eventManager.addListener(this._checkListener, 1);
this._am.checkUpdate();
this._updating = true;
},
hotUpdate: function () {
if (this._am) {
this._updateListener = new jsb.EventListenerAssetsManager(this._am, this.updateCb.bind(this));
cc.eventManager.addListener(this._updateListener, 1);
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
this._am.loadLocalManifest(this.manifestUrl);
}
this._failCount = 0;
this._am.update();
this._updating = true;
}
},
show: function () {
// if (this.updateUI.active === false) {
// this.updateUI.active = true;
// }
},
// use this for initialization
onLoad: function () {
// Hot update is only available in Native build
if (!cc.sys.isNative) {
return;
}
this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'xiaoming-remote-asset');
cc.log('Storage path for remote asset : ' + this._storagePath);
// Setup your own version compare handler, versionA and B is versions in string
// if the return value greater than 0, versionA is greater than B,
// if the return value equals 0, versionA equals to B,
// if the return value smaller than 0, versionA is smaller than B.
this.versionCompareHandle = function (versionA, versionB) {
cc.log("JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB);
var vA = versionA.split('.');
var vB = versionB.split('.');
for (var i = 0; i < vA.length; ++i) {
var a = parseInt(vA[i]);
var b = parseInt(vB[i] || 0);
if (a === b) {
continue;
}
else {
return a - b;
}
}
if (vB.length > vA.length) {
return -1;
}
else {
return 0;
}
};
// Init with empty manifest url for testing custom manifest
this._am = new jsb.AssetsManager('', this._storagePath, this.versionCompareHandle);
if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
this._am.retain();
}
// Setup the verification callback, but we don't have md5 check function yet, so only print some message
// Return true if the verification passed, otherwise return false
this._am.setVerifyCallback(function (path, asset) {
// When asset is compressed, we don't need to check its md5, because zip file have been deleted.
var compressed = asset.compressed;
// Retrieve the correct md5 value.
var expectedMD5 = asset.md5;
// asset.path is relative path and path is absolute.
var relativePath = asset.path;
// The size of asset file, but this value could be absent.
var size = asset.size;
if (compressed) {
cc.log("Verification passed : " + relativePath);
return true;
}
else {
cc.log("Verification passed : " + relativePath + ' (' + expectedMD5 + ')');
return true;
}
});
cc.log("Hot update is ready, please check or directly update.");
if (cc.sys.os === cc.sys.OS_ANDROID) {
// Some Android device may slow down the download process when concurrent tasks is too much.
// The value may not be accurate, please do more test and find what's most suitable for your game.
this._am.setMaxConcurrentTask(2);
cc.log("Max concurrent tasks count have been limited to 2");
}
// this.checkUpdate();
},
onDestroy: function () {
if (this._updateListener) {
cc.eventManager.removeListener(this._updateListener);
this._updateListener = null;
}
if (this._am && !cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
this._am.release();
}
}
});
3. 添加點(diǎn)擊事件
4. 構(gòu)建項目,生成原生資源文件
注意: 我們選擇的模板是 "default" ,發(fā)布路徑為 "./build" 谴垫,發(fā)布后的項目資源相對路徑為:build/jsb-default
5. 修改main.js(可省略)
?2018/08/23測試發(fā)現(xiàn)章母,構(gòu)建項目生成的main.js中,已包含判斷弹渔,不用修改胳施,也可以成功
根據(jù)官方文檔提示,每次構(gòu)建項目后肢专,都需要修改main.js,那我們就直接復(fù)制官方demo根目錄main.js的內(nèi)容覆蓋原有內(nèi)容焦辅。
? main.js 內(nèi)容如下:
main.js
(function () {
if (window.cc && cc.sys.isNative) {
var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
'use strict';
function boot () {
var settings = window._CCSettings;
window._CCSettings = undefined;
if ( !settings.debug ) {
// retrieve minified raw assets
var rawAssets = settings.rawAssets;
var assetTypes = settings.assetTypes;
for (var mount in rawAssets) {
var entries = rawAssets[mount];
for (var uuid in entries) {
var entry = entries[uuid];
var type = entry[1];
if (typeof type === 'number') {
entry[1] = assetTypes[type];
}
}
}
}
// init engine
var canvas;
if (cc.sys.isBrowser) {
canvas = document.getElementById('GameCanvas');
}
function setLoadingDisplay () {
// Loading splash scene
var splash = document.getElementById('splash');
var progressBar = splash.querySelector('.progress-bar span');
cc.loader.onProgress = function (completedCount, totalCount, item) {
var percent = 100 * completedCount / totalCount;
if (progressBar) {
progressBar.style.width = percent.toFixed(2) + '%';
}
};
splash.style.display = 'block';
progressBar.style.width = '0%';
cc.director.once(cc.Director.EVENT_AFTER_SCENE_LAUNCH, function () {
splash.style.display = 'none';
});
}
var onStart = function () {
cc.view.resizeWithBrowserSize(true);
// UC browser on many android devices have performance issue with retina display
if (cc.sys.os !== cc.sys.OS_ANDROID || cc.sys.browserType !== cc.sys.BROWSER_TYPE_UC) {
cc.view.enableRetina(true);
}
//cc.view.setDesignResolutionSize(settings.designWidth, settings.designHeight, cc.ResolutionPolicy.SHOW_ALL);
if (cc.sys.isBrowser) {
setLoadingDisplay();
}
if (cc.sys.isMobile) {
if (settings.orientation === 'landscape') {
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
}
else if (settings.orientation === 'portrait') {
cc.view.setOrientation(cc.macro.ORIENTATION_PORTRAIT);
}
// qq, wechat, baidu
cc.view.enableAutoFullScreen(
cc.sys.browserType !== cc.sys.BROWSER_TYPE_BAIDU &&
cc.sys.browserType !== cc.sys.BROWSER_TYPE_WECHAT &&
cc.sys.browserType !== cc.sys.BROWSER_TYPE_MOBILE_QQ
);
}
// init assets
cc.AssetLibrary.init({
libraryPath: 'res/import',
rawAssetsBase: 'res/raw-',
rawAssets: settings.rawAssets,
packedAssets: settings.packedAssets
});
var launchScene = settings.launchScene;
// load scene
if (cc.runtime) {
cc.director.setRuntimeLaunchScene(launchScene);
}
cc.director.loadScene(launchScene, null,
function () {
if (cc.sys.isBrowser) {
// show canvas
canvas.style.visibility = '';
var div = document.getElementById('GameDiv');
if (div) {
div.style.backgroundImage = '';
}
}
cc.loader.onProgress = null;
// play game
// cc.game.resume();
console.log('Success to load scene: ' + launchScene);
}
);
};
// jsList
var jsList = settings.jsList;
var bundledScript = settings.debug ? 'project.dev.js' : 'project.js';
if (jsList) {
jsList.push(bundledScript);
}
else {
jsList = [bundledScript];
}
// anysdk scripts
if (cc.sys.isNative && cc.sys.isMobile) {
jsList = jsList.concat(['jsb_anysdk.js', 'jsb_anysdk_constants.js']);
}
jsList = jsList.map(function (x) { return 'src/' + x; });
var option = {
//width: width,
//height: height,
id: 'GameCanvas',
scenes: settings.scenes,
debugMode: settings.debug ? cc.DebugMode.INFO : cc.DebugMode.ERROR,
showFPS: settings.debug,
frameRate: 60,
jsList: jsList,
groupList: settings.groupList,
collisionMatrix: settings.collisionMatrix,
renderMode: 0
};
cc.game.run(option, onStart);
}
if (window.document) {
var splash = document.getElementById('splash');
splash.style.display = 'block';
var cocos2d = document.createElement('script');
cocos2d.async = true;
cocos2d.src = window._CCSettings.debug ? 'cocos2d-js.js' : 'cocos2d-js-min.js';
var engineLoaded = function () {
document.body.removeChild(cocos2d);
cocos2d.removeEventListener('load', engineLoaded, false);
boot();
};
cocos2d.addEventListener('load', engineLoaded, false);
document.body.appendChild(cocos2d);
}
else if (window.jsb) {
require('src/settings.js');
require('src/jsb_polyfill.js');
boot();
}
})();
二博杖,服務(wù)器搭建(僅局域網(wǎng)內(nèi)可訪問)
1. 編寫服務(wù)器腳本
server.py
import SimpleHTTPServer
import SocketServer
PORT = 8000
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
httpd = SocketServer.TCPServer(("", PORT), Handler)
print "serving at port", PORT
httpd.serve_forever()
2. 啟動服務(wù)
? 打開命令行
? 切換目錄到 server.py 所在目錄下
? 輸入下方命令,啟動服務(wù):
python -m server 8000
? 啟動完成后筷登,服務(wù)器的地址即: http://192.168.200.117:8000
? 對應(yīng)的根目錄即為server.py所在的目錄下剃根,我們在服務(wù)器根目錄下,新建HotUpdate文件夾前方,用于存儲新版本資源狈醉,通過網(wǎng)頁訪問如下:
? 詳細(xì)參見官方文檔給出的地址:https://docs.python.org/2/library/simplehttpserver.html
三,生成舊版清單文件
1. 修改version_generator.js
packageUrl:服務(wù)器存放資源文件(src res)的路徑
remoteManifestUrl:服務(wù)器存放資源清單文件(project.manifest)的路徑
remoteVersionUrl:服務(wù)器存放version.manifest的路徑
dest:要生成的manifest文件存放路徑
src:項目構(gòu)建后的資源目錄
2. 根據(jù)構(gòu)建后的資源目錄惠险,執(zhí)行version_generator.js苗傅,生成manifest清單文件
? 打開cmd,切換到當(dāng)前項目根目錄下班巩,執(zhí)行下方命令:
//官方給出的命令格式
>node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/
//我的命令
>node version_generator.js -v 1.0.0 -u http://192.168.200.117:8000/HotUpdate/ -s build/jsb-default/ -d assets
//由于我們version_generator文件中渣慕,都配置好了參數(shù)
//因此可以簡單調(diào)用以下命令即可
>node version_generator.js
??生成的Manifest文件目錄,如下:
- -v: 指定 Manifest 文件的主版本號。
- -u: 指定服務(wù)器遠(yuǎn)程包的地址抱慌,這個地址需要和最初發(fā)布版本中 Manifest 文件的遠(yuǎn)程包地址一致逊桦,否則無法檢測到更新。
- -s: 本地原生打包版本的目錄相對路徑抑进。
- -d: 保存 Manifest 文件的地址强经。
PS:如果version_generator.js中的配置都正確寺渗,特別是版本號匿情,可以直接執(zhí)行 node version_generator.js。
3. 并綁定到熱更新腳本上
? 如果指定的Manifest文件生成的目錄不在assets下户秤,則需將project.manifest復(fù)制到assets目錄下
4. 打包舊版本
??構(gòu)建項目->編譯
??在真機(jī)上運(yùn)行build/jsb-default/simulator目錄下的apk
??1.0.0版本運(yùn)行如下:
三码秉,生成新版本
1. 更改代碼,更改version_generator.js中的版本號
2. 構(gòu)建項目 && 重新生成資源清單文件
??構(gòu)建項目:此步驟和生成舊版本中一樣,這里就不截圖啦府蔗。
??重新生成資源清單文件:修改version_generator.js中的版本號后晋控,可以直接調(diào)用以下命令:
>node version_generator.js
3. 將manifest文件以及src,res拷貝到服務(wù)器
4. 運(yùn)行舊版本
? 運(yùn)行結(jié)果姓赤,點(diǎn)擊 檢測更新 后赡译,很快app重啟,logo變成了head.png不铆。
? 成功蝌焚!
由于這里檢測到新版本后,就開始自動更新誓斥。
你可以修改這部分邏輯只洒,檢測到有新版后,彈窗提示是否需要更新劳坑。
后面再繼續(xù)研究 大廳+子游戲的模式毕谴,以及不重啟加載子游戲方案...