├── example ├── project.json ├── assets │ ├── script.meta │ └── script │ │ ├── AssetsDownload.js.meta │ │ ├── AssetsManager.js.meta │ │ ├── GameConfig.js.meta │ │ ├── HotUpdateManager.js.meta │ │ ├── GameConfig.js │ │ ├── HotUpdateManager.js │ │ ├── AssetsDownload.js │ │ └── AssetsManager.js ├── settings │ └── project.json ├── packages │ └── hot_update │ │ ├── main.js │ │ ├── package.json │ │ ├── comp-inspectors │ │ └── custom-comp.js │ │ └── panel │ │ ├── copy.js │ │ └── index.js ├── jsconfig.json └── .gitignore └── README.md /example/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "engine": "cocos-creator-js", 3 | "packages": "packages" 4 | } -------------------------------------------------------------------------------- /example/assets/script.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.0.1", 3 | "uuid": "ec32abaf-7a1b-465a-89f0-dc0286d623a0", 4 | "isGroup": false, 5 | "subMetas": {} 6 | } -------------------------------------------------------------------------------- /example/assets/script/AssetsDownload.js.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.0.2", 3 | "uuid": "492f2922-4c28-4f79-a4c0-a77b381ba7d3", 4 | "isPlugin": false, 5 | "subMetas": {} 6 | } -------------------------------------------------------------------------------- /example/assets/script/AssetsManager.js.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.0.2", 3 | "uuid": "37490f16-4cc3-4009-8a4f-9e2e0d95f105", 4 | "isPlugin": false, 5 | "subMetas": {} 6 | } -------------------------------------------------------------------------------- /example/assets/script/GameConfig.js.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.0.2", 3 | "uuid": "8cb1a2b4-669f-4b9e-ac9d-072516d827e8", 4 | "isPlugin": false, 5 | "subMetas": {} 6 | } -------------------------------------------------------------------------------- /example/assets/script/HotUpdateManager.js.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.0.2", 3 | "uuid": "7ffddbcc-f926-45a4-bd4a-99e1130c95af", 4 | "isPlugin": false, 5 | "subMetas": {} 6 | } -------------------------------------------------------------------------------- /example/settings/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "start-scene": "current", 3 | "group-list": [ 4 | "default" 5 | ], 6 | "collision-matrix": [ 7 | [ 8 | true 9 | ] 10 | ], 11 | "excluded-modules": [] 12 | } -------------------------------------------------------------------------------- /example/packages/hot_update/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | load () { 5 | }, 6 | 7 | unload () { 8 | }, 9 | 10 | messages: { 11 | open() { 12 | Editor.Panel.open('hot_update'); 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /example/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs" 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "library", 9 | "local", 10 | "settings", 11 | "temp" 12 | ] 13 | } -------------------------------------------------------------------------------- /example/packages/hot_update/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hot_update", 3 | "main": "main.js", 4 | "main-menu": { 5 | "自定义工具/热更配置生成": { 6 | "message": "hot_update:open" 7 | } 8 | }, 9 | "panel": { 10 | "main" : "panel/index.js", 11 | "type" : "dockable", 12 | "title" : "热更配置生成", 13 | "width" : 400, 14 | "height": 300 15 | } 16 | } -------------------------------------------------------------------------------- /example/packages/hot_update/comp-inspectors/custom-comp.js: -------------------------------------------------------------------------------- 1 | Vue.component('custom-comp-inspector', { 2 | template: ` 3 | 4 | 5 | 6 | 7 | `, 8 | 9 | props: { 10 | target: { 11 | twoWay: true, 12 | type: Object, 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /example/assets/script/GameConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局环境设置 3 | * Author : donggang 4 | * Create Time : 2016.7.26 5 | */ 6 | /** 外网测试服 */ 7 | var debug_extranet = { 8 | gateSocketIp : "121.41.4.17", // 网关地址 9 | gateSocketPort : 3101, // 网关端口 10 | 11 | useSSL : false, // 是否使用https 12 | textUpdate : true, // 是否开启测试热更新 13 | }; 14 | 15 | window.game = window.game || {}; 16 | game.config = module.exports = debug_extranet; 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cocos Creator 脚本热更新 2 | 通过脚本实现了官方 C++ 版热更功能,提高的稳定性和易维护性,使用者可在脚本上扩展自定义的版本更新业务。 3 | 4 | # 实现功能清单 5 | 1. 版本配置文件对比,提示有新版本 6 | 2. 版本清单文件对比,分析出添加、更新、删除的资源进行下载或删除 7 | 3. 更新失败或异常中断时,记录更新状态,下次触发更新时恢复更新进度,不在下载已更新的文件 8 | 4. 支持多模块热更新管理 9 | 5. 基于CocosCrator编辑器的版本配置文件生成工具插件 10 | 11 | # 使用方式 12 | 1. 打开 Cocos Creator 的 XXGAME 项目 13 | 2. 菜单选择“项目 > 构建发布” 14 | 3. 点击构建按钮 15 | 4. 菜单选择“自定义工具 > 热更配置生成” 16 | 17 | # 其它说明 18 | 1. 热更脚本代码在assets/script文件下AssetsManager.js、AssetsDownload.js两个文件,代码上注释比较全 19 | 2. 热更工具代码在packages/hot_update文件下 20 | 21 | # 联系方式 22 | gdong@bainainfo.com -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | #///////////////////////////////////////////////////////////////////////////// 2 | # Fireball Projects 3 | #///////////////////////////////////////////////////////////////////////////// 4 | 5 | library/ 6 | temp/ 7 | local/ 8 | build/ 9 | 10 | #///////////////////////////////////////////////////////////////////////////// 11 | # Logs and databases 12 | #///////////////////////////////////////////////////////////////////////////// 13 | 14 | *.log 15 | *.sql 16 | *.sqlite 17 | 18 | #///////////////////////////////////////////////////////////////////////////// 19 | # files for debugger 20 | #///////////////////////////////////////////////////////////////////////////// 21 | 22 | *.sln 23 | *.csproj 24 | *.pidb 25 | *.unityproj 26 | *.suo 27 | 28 | #///////////////////////////////////////////////////////////////////////////// 29 | # OS generated files 30 | #///////////////////////////////////////////////////////////////////////////// 31 | 32 | .DS_Store 33 | ehthumbs.db 34 | Thumbs.db 35 | 36 | #///////////////////////////////////////////////////////////////////////////// 37 | # exvim files 38 | #///////////////////////////////////////////////////////////////////////////// 39 | 40 | *UnityVS.meta 41 | *.err 42 | *.err.meta 43 | *.exvim 44 | *.exvim.meta 45 | *.vimentry 46 | *.vimentry.meta 47 | *.vimproject 48 | *.vimproject.meta 49 | .vimfiles.*/ 50 | .exvim.*/ 51 | quick_gen_project_*_autogen.bat 52 | quick_gen_project_*_autogen.bat.meta 53 | quick_gen_project_*_autogen.sh 54 | quick_gen_project_*_autogen.sh.meta 55 | .exvim.app 56 | 57 | #///////////////////////////////////////////////////////////////////////////// 58 | # webstorm files 59 | #///////////////////////////////////////////////////////////////////////////// 60 | 61 | .idea/ 62 | -------------------------------------------------------------------------------- /example/assets/script/HotUpdateManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 热更新管理 3 | * Author : donggang 4 | * Create Time : 2016.7.29 5 | * 6 | * 需求说明: 7 | * 1、可后台更新版本资源 8 | */ 9 | 10 | var AssetsManager = require("AssetsManager"); 11 | 12 | var amManager = cc.Class({ 13 | ctor : function(){ 14 | this._updates = {}; // 更新模块集合 15 | this._queue = []; // 更新对列 16 | this._isUpdating = false; // 是否正在更新中 17 | this._current = null; // 当前正在更新的模块 18 | this._noComplete = {}; // 上次未完成的热更项 19 | }, 20 | 21 | /** 22 | * 获取模块版本信息 23 | * @param localVersionCb(function) 本地版本信息加载完成 24 | * @param remoteVersionCb(function) 远程版本信息加载完成 25 | * 26 | */ 27 | getModules : function(remoteVersionCb){ 28 | if (!cc.sys.isNative) { 29 | if (remoteVersionCb) remoteVersionCb(); 30 | return; 31 | } 32 | 33 | game.asset.check(function(modules, versions){ 34 | this.modules = modules; 35 | this.versions = versions; 36 | if (remoteVersionCb) remoteVersionCb(); 37 | }.bind(this)); 38 | }, 39 | 40 | /** 载入没更新完的模块状态 */ 41 | load : function(){ 42 | if (!cc.sys.isNative) return; 43 | 44 | var data = cc.sys.localStorage.getItem("update_no_complete"); 45 | if (data){ 46 | var json = JSON.parse(data); 47 | for(var i = 0; i < json.length; i++){ 48 | this._noComplete[json[i]] = json[i]; 49 | } 50 | cc.sys.localStorage.removeItem("update_no_complete"); 51 | } 52 | }, 53 | 54 | getProgress : function(name){ 55 | var am = this._updates[name]; 56 | return am.getProgress(); 57 | }, 58 | 59 | /** 60 | * 初始化更新模块名 61 | * @param name(string) 模块名 62 | * @param onCheckComplete(function) 检查版本完成 63 | * @param onComplete(function) 模块名 64 | * @param onProgress(function) 更新完成 65 | * @param onNewVersion(function) 已是最新版本 66 | */ 67 | init : function(name, onCheckComplete, onComplete, onProgress, onNewVersion) { 68 | game.AssetConfig.concurrent = 2; 69 | 70 | var am = new AssetsManager(); 71 | am.name = name; 72 | am.on(game.AssetEvent.NEW_VERSION , onNewVersion); 73 | am.on(game.AssetEvent.PROGRESS , onProgress); 74 | am.on(game.AssetEvent.FAILD , this._onFailed.bind(this)); 75 | am.on(game.AssetEvent.NEW_VERSION_FOUND , this._onCheckComplete.bind(this)); 76 | am.on(game.AssetEvent.SUCCESS , this._onUpdateComplete.bind(this)); 77 | am.on(game.AssetEvent.REMOTE_VERSION_MANIFEST_LOAD_FAILD, this._onNetError.bind(this)); 78 | am.on(game.AssetEvent.REMOTE_PROJECT_MANIFEST_LOAD_FAILD, this._onNetError.bind(this)); 79 | am.on(game.AssetEvent.NO_NETWORK , this._onNetError.bind(this)); 80 | am.onCheckComplete = onCheckComplete; 81 | am.onComplete = onComplete; 82 | 83 | this._updates[name] = am; 84 | }, 85 | 86 | /** 是否没完成 */ 87 | isNoComplete : function(name){ 88 | if (this._noComplete[name] == null) 89 | return false; 90 | 91 | return true; 92 | }, 93 | 94 | /** 95 | * 检查版本是否需要更新 96 | */ 97 | check : function(name){ 98 | var am = this._updates[name]; 99 | am.check(name); 100 | }, 101 | 102 | /** 断网后恢复状态 */ 103 | recovery : function(name){ 104 | if (this._current && this._isUpdating == false){ 105 | this._isUpdating = true; 106 | this._current.recovery(); 107 | } 108 | }, 109 | 110 | _onFailed : function(event){ 111 | this._isUpdating = false; 112 | event.target.check(event.target.name); 113 | }, 114 | 115 | _onNetError : function(event){ 116 | this._isUpdating = false; 117 | }, 118 | 119 | /** 检查版本完成 */ 120 | _onCheckComplete : function(event){ 121 | this._queue.push(event.target); 122 | 123 | // 保存下在下载的模块状态 124 | this._saveNoCompleteModule(); 125 | 126 | if (event.target.onCheckComplete) event.target.onCheckComplete(); 127 | 128 | if (this._isUpdating == false){ 129 | this._isUpdating = true; 130 | this._current = event.target; 131 | this._current.update(); 132 | } 133 | }, 134 | 135 | _onUpdateComplete : function(event){ 136 | if (event.target.onComplete) event.target.onComplete(); 137 | 138 | // 删除当前完成的更新对象 139 | this._queue.shift(); 140 | this._isUpdating = false; 141 | 142 | // 保存下在下载的模块状态 143 | this._saveNoCompleteModule(); 144 | 145 | // 更新对列中下一个更新对象 146 | if (this._queue.length > 0){ 147 | this._isUpdating = true; 148 | this._current = this._queue[0]; 149 | this._current.update(); 150 | } 151 | }, 152 | 153 | // 保存下在下载的模块状态 154 | _saveNoCompleteModule : function(){ 155 | var names = []; 156 | for(var i = 0; i < this._queue.length; i++){ 157 | names.push(this._queue[i].name); 158 | } 159 | cc.sys.localStorage.setItem("update_no_complete", JSON.stringify(names)); 160 | } 161 | }); -------------------------------------------------------------------------------- /example/packages/hot_update/panel/copy.js: -------------------------------------------------------------------------------- 1 | //运行需要安装 async.js npm install async 2 | //更新完成后,拷贝发布文件到HTTP服务器更新目录 3 | var fs = require('fs'); 4 | var path = require("path"); 5 | var async = require("async"); 6 | 7 | // cursively make dir 8 | function mkdirs(p, mode, f, made) { 9 | if (typeof mode === 'function' || mode === undefined) { 10 | f = mode; 11 | mode = 0777 & (~process.umask()); 12 | } 13 | if (!made) 14 | made = null; 15 | 16 | var cb = f || function () {}; 17 | if (typeof mode === 'string') 18 | mode = parseInt(mode, 8); 19 | p = path.resolve(p); 20 | 21 | fs.mkdir(p, mode, function (er) { 22 | if (!er) { 23 | made = made || p; 24 | return cb(null, made); 25 | } 26 | switch (er.code) { 27 | case 'ENOENT': 28 | mkdirs(path.dirname(p), mode, function (er, made) { 29 | if (er) { 30 | cb(er, made); 31 | } else { 32 | mkdirs(p, mode, cb, made); 33 | } 34 | }); 35 | break; 36 | 37 | // In the case of any other error, just see if there's a dir 38 | // there already. If so, then hooray! If not, then something 39 | // is borked. 40 | default: 41 | fs.stat(p, function (er2, stat) { 42 | // if the stat fails, then that's super weird. 43 | // let the original error be the failure reason. 44 | if (er2 || !stat.isDirectory()) { 45 | cb(er, made); 46 | } else { 47 | cb(null, made) 48 | }; 49 | }); 50 | break; 51 | } 52 | }); 53 | } 54 | // single file copy 55 | function copyFile(file, toDir, cb) { 56 | async.waterfall([ 57 | function (callback) { 58 | fs.exists(toDir, function (exists) { 59 | if (exists) { 60 | callback(null, false); 61 | } else { 62 | callback(null, true); 63 | } 64 | }); 65 | }, function (need, callback) { 66 | if (need) { 67 | mkdirs(path.dirname(toDir), callback); 68 | } else { 69 | callback(null, true); 70 | } 71 | }, function (p, callback) { 72 | var reads = fs.createReadStream(file); 73 | var writes = fs.createWriteStream(path.join(path.dirname(toDir), path.basename(file))); 74 | reads.pipe(writes); 75 | //don't forget close the when all the data are read 76 | reads.on("end", function () { 77 | writes.end(); 78 | callback(null); 79 | }); 80 | reads.on("error", function (err) { 81 | console.log("error occur in reads"); 82 | callback(true, err); 83 | }); 84 | 85 | } 86 | ], cb); 87 | 88 | } 89 | 90 | // cursively count the files that need to be copied 91 | 92 | function _ccoutTask(from, to, cbw) { 93 | async.waterfall([ 94 | function (callback) { 95 | fs.stat(from, callback); 96 | }, 97 | function (stats, callback) { 98 | if (stats.isFile()) { 99 | cbw.addFile(from, to); 100 | callback(null, []); 101 | } else if (stats.isDirectory()) { 102 | fs.readdir(from, callback); 103 | } 104 | }, 105 | function (files, callback) { 106 | if (files.length) { 107 | for (var i = 0; i < files.length; i++) { 108 | _ccoutTask(path.join(from, files[i]), path.join(to, files[i]), cbw.increase()); 109 | } 110 | } 111 | callback(null); 112 | } 113 | ], cbw); 114 | 115 | } 116 | // wrap the callback before counting 117 | function ccoutTask(from, to, cb) { 118 | var files = []; 119 | var count = 1; 120 | 121 | function wrapper(err) { 122 | count--; 123 | if (err || count <= 0) { 124 | cb(err, files) 125 | } 126 | } 127 | wrapper.increase = function () { 128 | count++; 129 | return wrapper; 130 | } 131 | wrapper.addFile = function (file, dir) { 132 | files.push({ 133 | file : file, 134 | dir : dir 135 | }); 136 | } 137 | 138 | _ccoutTask(from, to, wrapper); 139 | } 140 | 141 | /** 拷贝目录 */ 142 | copyDir = function (from, to, cb) { 143 | if(!cb){ 144 | cb=function(){}; 145 | } 146 | async.waterfall([ 147 | function (callback) { 148 | fs.exists(from, function (exists) { 149 | if (exists) { 150 | callback(null, true); 151 | } else { 152 | console.log(from + " not exists"); 153 | callback(true); 154 | } 155 | }); 156 | }, 157 | function (exists, callback) { 158 | fs.stat(from, callback); 159 | }, 160 | function (stats, callback) { 161 | if (stats.isFile()) { 162 | // one file copy 163 | copyFile(from, to, function (err) { 164 | if (err) { 165 | // break the waterfall 166 | callback(true); 167 | } else { 168 | callback(null, []); 169 | } 170 | }); 171 | } else if (stats.isDirectory()) { 172 | ccoutTask(from, to, callback); 173 | } 174 | }, 175 | function (files, callback) { 176 | // prevent reaching to max file open limit 177 | async.mapLimit(files, 10, function (f, cb) { 178 | copyFile(f.file, f.dir, cb); 179 | }, callback); 180 | } 181 | ], cb); 182 | } -------------------------------------------------------------------------------- /example/assets/script/AssetsDownload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 队列加载资源 3 | * Author : donggang 4 | * Create Time : 2016.12.15 5 | * 6 | * 事件: 7 | * this.onProgress 更新进度 8 | * this.onNoNetwork 断网 9 | * this.onComplete 更新完成 10 | */ 11 | 12 | module.exports = cc.Class({ 13 | /** 分析并获取需要更新的资源 */ 14 | download: function (storagePath, localManifest, remoteManifest) { 15 | this._storagePath = storagePath; 16 | this._localManifest = localManifest; 17 | this._remoteManifest = remoteManifest; 18 | 19 | this._nocache = (new Date()).getTime(); 20 | 21 | this._downloadUnits = []; // 下载文件对象集合 22 | this._completeUnits = []; // 已下载完成对象集合 23 | this._failedUnits = []; // 下载失败文件对象集合 24 | this._deleteUnits = []; // 需要删除文件对象集合 25 | 26 | this._downloadComplete = 0; // 下载完成的文件数量 27 | this._downloadFailed = 0; // 下载失败的文件数量 28 | this._failCount = 0; // 下载失败的次数 29 | this._concurrentCurrent = 0; // 并发数量当前值 30 | 31 | this._analysisDownloadUnits(); 32 | this._analysisDeleteUnits(); 33 | 34 | // 当前总更新单位数量 = 更新完成文件数量 + 待更新文件数量 35 | this._totalUnits = this._downloadComplete + this._downloadUnits.length; 36 | 37 | cc.log("【更新】共有{0}个文件需要更新".format(this._downloadUnits.length)); 38 | cc.log("【更新】共有{0}个文件需要删除".format(this._deleteUnits.length)); 39 | 40 | this._items = this._downloadUnits.slice(0); 41 | 42 | if (this._items.length > 0){ 43 | this._downloadAsset(); 44 | } 45 | else{ 46 | cc.log("【更新】无更新文件,更新完成"); 47 | if (this.onComplete) this.onComplete(); 48 | } 49 | }, 50 | 51 | /** 对比本地项目清单数据和服务器清单数据,找出本地缺少的以及和服务器不一样的资源 */ 52 | _analysisDownloadUnits : function(){ 53 | for (var key in this._remoteManifest.assets) { 54 | if (this._localManifest.assets.hasOwnProperty(key)) { 55 | if (game.AssetConfig.debugRes || this._remoteManifest.assets[key].md5 != this._localManifest.assets[key].md5) { 56 | // cc.log("【更新】准备下载更新的资源 {0}".format(key)); 57 | this._addDownloadUnits(key); 58 | } 59 | } 60 | else { 61 | // cc.log("【更新】准备下载本是不存在的资源 {0}".format(key)); 62 | this._addDownloadUnits(key); 63 | } 64 | } 65 | }, 66 | 67 | /** 对比本地项目清单数据和服务器清单数据,找出本地多出的资源 */ 68 | _analysisDeleteUnits : function(){ 69 | for (var key in this._localManifest.assets) { 70 | if (this._remoteManifest.assets.hasOwnProperty(key) == false) { 71 | // cc.log("【更新】准备删除的资源{0}".format(key)); 72 | this._deleteUnits.push(key); 73 | } 74 | } 75 | }, 76 | 77 | /** 添加下载单位 */ 78 | _addDownloadUnits : function(key){ 79 | if (this._remoteManifest.assets[key].state != true){ 80 | this._downloadUnits.push(key); // 远程版本的文件 MD5 值和本地不同时文件需要下载 81 | } 82 | else { 83 | this._downloadComplete++; // 恢复状态时的下载完成数量 84 | } 85 | }, 86 | 87 | /** 断网后恢复更新状态 */ 88 | recovery : function(){ 89 | this._downloadAsset(); 90 | }, 91 | 92 | /** 下载资源 */ 93 | _downloadAsset : function(){ 94 | if (game.config.textUpdate) this._remoteManifest.server = game.AssetConfig.testCdn; 95 | 96 | var relativePath = this._items.shift(); 97 | var url = cc.path.join(this._remoteManifest.server, game.AssetConfig.line, relativePath); 98 | 99 | // 下载成功 100 | var complete = function (asset) { 101 | // 文件保存到本地 102 | this._saveAsset(relativePath, asset); 103 | 104 | // 记录更新完成的文件 105 | this._completeUnits.push(relativePath); 106 | 107 | // 下载完成的文件数量加 1 108 | this._downloadComplete++; 109 | 110 | if (game.AssetConfig.debugProgress) 111 | cc.log("【更新】进度 {0}/{1},当前有 {2} 个资源并行下载".format(this._downloadComplete, this._totalUnits, this._concurrentCurrent)); 112 | 113 | // 还原并发数量 114 | this._concurrentCurrent--; 115 | 116 | // 更新进度事件 117 | if (this.onProgress) { 118 | this.onProgress(relativePath, this._downloadComplete / this._totalUnits); 119 | } 120 | 121 | // 判断是否下载完成 122 | this._isUpdateCompleted(); 123 | }.bind(this); 124 | 125 | // 下载失败 126 | var error = function (error) { 127 | this._failedUnits.push(relativePath); 128 | this._concurrentCurrent--; 129 | this._downloadFailed++; 130 | 131 | if (game.HttpEvent.NO_NETWORK){ // 触发断网事件 132 | if (this._concurrentCurrent == 0){ 133 | if (this.onNoNetwork) this.onNoNetwork(); 134 | } 135 | } 136 | else { 137 | cc.log("【更新】下载远程路径为 {0} 的文件失败,错误码为 {1}".format(url, error)); 138 | cc.log("【更新】进度 {0}/{1}, 总处理文件数据为 {2}".format(this._downloadComplete, this._totalUnits, this._downloadComplete + this._downloadFailed)); 139 | 140 | this._isUpdateCompleted(); 141 | } 142 | }.bind(this); 143 | 144 | game.http.getByArraybuffer(this._noCache(url), complete, error); 145 | 146 | // 开启一个并行下载队列 147 | this._concurrentCurrent++; 148 | if (this._concurrentCurrent < game.AssetConfig.concurrent){ 149 | this._downloadAsset(); 150 | } 151 | }, 152 | 153 | /** 下载失败的资源 */ 154 | _downloadFailedAssets: function () { 155 | // 下载失败的文件数量重置 156 | this._downloadFailed = 0; 157 | this._downloadUnits = this._failedUnits; 158 | this._failedUnits = []; 159 | this._items = this._downloadUnits.slice(0); 160 | 161 | if (this._items.length > 0){ 162 | this._downloadAsset(); 163 | } 164 | }, 165 | 166 | /** 判断是否全部更新完成 */ 167 | _isUpdateCompleted: function () { 168 | var handleCount = this._downloadComplete + this._downloadFailed; // 处理完成数量 169 | 170 | if (this._totalUnits == this._downloadComplete) { // 全下载完成 171 | cc.log("【更新】更新完成"); 172 | 173 | // 触发热更完成事件 174 | if (this.onComplete) this.onComplete(); 175 | 176 | // 删除本地比服务器多出的文件 177 | this._deleteAssets(); 178 | } 179 | else if (this._totalUnits == handleCount) { // 全处理完成,有下载失败的文件,需要重试 180 | cc.log("【更新】下载文件总数量  :", this._totalUnits); 181 | cc.log("【更新】下载成功的文件数量:", this._downloadComplete); 182 | cc.log("【更新】下载失败的文件数量:", this._downloadFailed); 183 | 184 | // 更新失败的次数加 1 185 | this._failCount++; 186 | 187 | if (this._failCount < 3) { 188 | cc.log("【更新】更新重试第 {0} 次".format(this._failCount)); 189 | 190 | this._downloadFailedAssets(); 191 | } 192 | else { 193 | cc.log("【更新】更新失败"); 194 | 195 | // 触发热更失败事件 196 | if (this.onFaild) this.onFaild(); 197 | } 198 | } 199 | else if (this._items.length > 0 && this._concurrentCurrent < game.AssetConfig.concurrent) { // 队列下载 200 | this._downloadAsset(); 201 | } 202 | }, 203 | 204 | /** 删除本地比服务器多出的文件 */ 205 | _deleteAssets: function () { 206 | for (var i = 0; i < this._deleteUnits.length; i++) { 207 | var relativePath = this._deleteUnits[i]; 208 | var filePath = cc.path.join(this._storagePath, relativePath); 209 | if (jsb.fileUtils.removeFile(filePath)) { 210 | cc.log("【更新】版本多余资源 {0} 删除成功".format(filePath)); 211 | } 212 | else { 213 | cc.log("【更新】版本多余资源 {0} 删除失败".format(filePath)); 214 | }; 215 | } 216 | }, 217 | 218 | /** 文件保存到本地 */ 219 | _saveAsset : function(relativePath, asset){ 220 | if (cc.sys.isNative){ 221 | var storeDirectory = cc.path.join(this._storagePath, relativePath.substr(0, relativePath.lastIndexOf("/"))); 222 | var storePath = cc.path.join(this._storagePath, relativePath); 223 | 224 | // 存储目录 225 | if (jsb.fileUtils.isDirectoryExist(storeDirectory) == false) { 226 | jsb.fileUtils.createDirectory(storeDirectory); 227 | } 228 | 229 | // 存储文件 230 | jsb.fileUtils.writeDataToFile(new Uint8Array(asset), storePath); 231 | } 232 | }, 233 | 234 | /** 规避 HTTP 缓存问题 */ 235 | _noCache: function (url) { 236 | return url + "?t=" + this._nocache; 237 | } 238 | }) -------------------------------------------------------------------------------- /example/packages/hot_update/panel/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 模块引用 */ 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var crypto = require('crypto'); 7 | 8 | require(path.join(Editor.projectInfo.path, 'packages/hot_update/panel/copy.js')); 9 | 10 | // 版本配置 11 | var config = { 12 | "isUpdateVersion" : true, 13 | "line" : "line1", // 版本线路文件夹,用于更新时优先更新没有用的线路,测试成功后切换热更 14 | "server" : "http://172.18.254.56:8080/update/", 15 | "modules": { 16 | "common" : { 17 | initial : "1.0.0.0015", // 发安装包时,initial 值和 initial 的值保持一致 18 | current : "1.0.0.0015" 19 | }, 20 | // 农场主题 21 | "10001" : { 22 | initial : "1.0.0.0001", 23 | current : "1.0.0.0001" 24 | }, 25 | // 水晶主题 26 | "10002" : { 27 | initial : "0", 28 | current : "1.0.0.0001" 29 | }, 30 | // 中国风主题 31 | "10003" : { 32 | initial : "0", 33 | current : "1.0.0.0001" 34 | }, 35 | // 社交主题 36 | "11001" : { 37 | initial : "0", 38 | current : "1.0.0.0001" 39 | } 40 | } 41 | } 42 | 43 | 44 | // 输入参数 45 | var args = {}; 46 | args.versionInitialPackPlayId = "10001"; // 初始版本打包进去的玩法编号 47 | args.server = config.server; 48 | args.releasePath = path.join(Editor.projectInfo.path, "build/jsb-default/"); // 发布资源文件路径 49 | args.projectVersionInargsPath = path.join(Editor.projectInfo.path, "assets/resources/version/"); // 项目初始热更配置文件路径 50 | args.releaseVersionInargsPath = path.join(args.releasePath , "res/raw-assets/resources/version"); // 发布初始热更配置文件路径 51 | args.releaseCdnPath = path.join(Editor.projectInfo.path, "../game-slots-update/update/", config.line, "/"); // 发布到CND路径 52 | args.releaseVersionConfigPath = path.join(Editor.projectInfo.path, "../game-slots-update-version/constinfo/version.json"); // 发布的所有模块版本信息 53 | 54 | /*----------------------------------------------------------------------------------------------------*/ 55 | 56 | // 生成初始热更配置 57 | var VersionInitial = function(moduleName, version, mPaths) { 58 | var mPaths = mPaths; 59 | 60 | var projectName = moduleName + "_project.manifest"; 61 | var versionName = moduleName + "_version.manifest"; 62 | 63 | var data = { 64 | server : args.server, 65 | remoteManifest : projectName, 66 | remoteVersion : versionName, 67 | version : version, 68 | assets : {}, 69 | searchPaths : [] 70 | } 71 | 72 | /** 处理配置信息 */ 73 | this.process = function(){ 74 | this.traversal(path.join(args.releasePath, 'res'), data.assets, moduleName); 75 | 76 | if (moduleName == "common") 77 | this.traversal(path.join(args.releasePath, 'src'), data.assets); 78 | 79 | var projectManifest = path.join(args.projectVersionInargsPath, projectName); 80 | var releaseManifest = path.join(args.releaseVersionInargsPath, projectName); 81 | 82 | // 生成配置配置文件 83 | fs.writeFile(projectManifest, JSON.stringify(data), (err) => { 84 | if (err) throw err; 85 | // Editor.log("生成初始热更配置文件 : " + projectName); 86 | }); 87 | 88 | // 拷贝到发布目录 89 | copyDir(projectManifest, releaseManifest, function (err) { 90 | if (err) Editor.log("生成初始热更配置文件出错 : \r\n" + path.resolve(releaseManifest)); 91 | else { 92 | Editor.log("生成初始热更配置文件到发布目录 : " + projectName); 93 | } 94 | }); 95 | } 96 | 97 | /** 98 | * 避难遍历文件和目录 99 | * @param folder(string) 目录 100 | * @param data(object) 数据 101 | * @param moduleName(string) 模块名 102 | */ 103 | this.traversal = function(folder, data, moduleName) { 104 | var stat = fs.statSync(folder); 105 | if (!stat.isDirectory()) { 106 | return; 107 | } 108 | 109 | var mPath = "resources/" + moduleName + "/"; // 模块路径 110 | var subpaths = fs.readdirSync(folder), subpath, md5, relative; 111 | for (var i = 0; i < subpaths.length; ++i) { 112 | if (subpaths[i][0] === '.') { 113 | continue; 114 | } 115 | subpath = path.join(folder, subpaths[i]); 116 | stat = fs.statSync(subpath); 117 | 118 | if (stat.isDirectory()) { 119 | this.traversal(subpath, data, moduleName); 120 | } 121 | else if (stat.isFile()) { 122 | md5 = crypto.createHash('md5').update(fs.readFileSync(subpath, 'utf8')).digest('hex'); 123 | relative = path.relative(args.releasePath, subpath); 124 | relative = relative.replace(/\\/g, '/'); 125 | 126 | if (relative.indexOf("src/") > -1){ 127 | data[relative] = {'md5' : md5}; 128 | } 129 | else if(relative.indexOf("res/") > -1){ 130 | if (moduleName.indexOf("common") == -1 && relative.indexOf(mPath) > -1 && moduleName == args.versionInitialPackPlayId){ // 不为主模块 131 | data[relative] = {'md5' : md5}; 132 | } 133 | else if (moduleName.indexOf("common") > -1 && 134 | (relative.indexOf(mPath) > -1 || relative.indexOf("/import/") > -1 || relative.indexOf("/raw-internal/") > -1)){ 135 | data[relative] = {'md5' : md5}; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | // 发布游戏初始版本配置文件 144 | function releaseVersionInitial(){ 145 | // 找到目标模块目录下的所有文件 146 | var mPaths = []; // 模块名 147 | var mVersions = []; // 版本号 148 | for(var moduleNmae in config.modules){ 149 | mPaths.push(moduleNmae); 150 | mVersions.push(config.modules[moduleNmae].initial); 151 | } 152 | 153 | // 生成不同模块的更新配置文件 154 | for(var i = 0; i < mPaths.length; i++){ 155 | var vi = new VersionInitial(mPaths[i], mVersions[i], mPaths); 156 | vi.process(); 157 | } 158 | 159 | // 发布CDN版本更新资源 160 | releaseVersionCdn(); 161 | } 162 | 163 | /*----------------------------------------------------------------------------------------------------*/ 164 | 165 | // 生成初始热更配置 166 | var VersionCdn = function(moduleName, version) { 167 | var projectName = moduleName + "_project.manifest"; 168 | var versionName = moduleName + "_version.manifest"; 169 | 170 | var data = { 171 | server : args.server, 172 | remoteManifest : projectName, 173 | remoteVersion : versionName, 174 | version : version, 175 | assets : {}, 176 | searchPaths : [] 177 | } 178 | 179 | /** 处理配置信息 */ 180 | this.process = function(){ 181 | this.traversal(path.join(args.releasePath, 'res'), data.assets, moduleName); 182 | 183 | if (moduleName == "common") 184 | this.traversal(path.join(args.releasePath, 'src'), data.assets); 185 | 186 | var projectProjectManifest = path.join(args.releaseCdnPath, projectName); 187 | 188 | // 生成配置配置文件 project.manifest 189 | fs.writeFile(projectProjectManifest, JSON.stringify(data), (err) => { 190 | if (err) throw err; 191 | // Editor.log("生成CDN更配置文件 : " + projectName); 192 | }); 193 | 194 | var projectVersionManifest = path.join(args.releaseCdnPath, versionName); 195 | 196 | delete data.assets; 197 | delete data.searchPaths; 198 | 199 | // 生成配置配置文件 version.manifest 200 | fs.writeFile(projectVersionManifest, JSON.stringify(data), (err) => { 201 | if (err) throw err; 202 | // Editor.log("生成CDN更配置文件 : " + versionName); 203 | }); 204 | } 205 | 206 | /** 207 | * 避难遍历文件和目录 208 | * @param folder(string) 目录 209 | * @param data(object) 数据 210 | * @param moduleName(string) 模块名 211 | */ 212 | this.traversal = function(folder, data, moduleName) { 213 | var stat = fs.statSync(folder); 214 | if (!stat.isDirectory()) { 215 | return; 216 | } 217 | 218 | var mPath = "resources/" + moduleName + "/"; // 模块路径 219 | var subpaths = fs.readdirSync(folder), subpath, md5, relative; 220 | for (var i = 0; i < subpaths.length; ++i) { 221 | if (subpaths[i][0] === '.') { 222 | continue; 223 | } 224 | subpath = path.join(folder, subpaths[i]); 225 | stat = fs.statSync(subpath); 226 | 227 | if (stat.isDirectory()) { 228 | this.traversal(subpath, data, moduleName); 229 | } 230 | else if (stat.isFile()) { 231 | md5 = crypto.createHash('md5').update(fs.readFileSync(subpath, 'utf8')).digest('hex'); 232 | relative = path.relative(args.releasePath, subpath); 233 | relative = relative.replace(/\\/g, '/'); 234 | 235 | if (relative.indexOf("src/") > -1){ 236 | data[relative] = {'md5' : md5}; 237 | } 238 | else if(relative.indexOf("res/") > -1){ 239 | if (moduleName.indexOf("common") == -1 && relative.indexOf(mPath) > -1){ // 不为主模块 240 | data[relative] = {'md5' : md5}; 241 | } 242 | else if (moduleName.indexOf("common") > -1 && 243 | (relative.indexOf(mPath) > -1 || relative.indexOf("/import/") > -1 || relative.indexOf("/raw-internal/") > -1)){ 244 | data[relative] = {'md5' : md5}; 245 | } 246 | } 247 | } 248 | } 249 | } 250 | } 251 | 252 | // 发布CDN版本更新资源 253 | function releaseVersionCdn(){ 254 | var mPaths = []; // 模块名 255 | var mVersions = []; // 版本号 256 | var playData = {}; // 玩法版本信息 257 | playData.line = config.line; 258 | playData.modules = {}; 259 | for(var moduleNmae in config.modules){ 260 | mPaths.push(moduleNmae); 261 | mVersions.push(config.modules[moduleNmae].current); 262 | 263 | playData.modules[moduleNmae] = config.modules[moduleNmae].current; 264 | } 265 | 266 | // 生成CDN所有版本配置文件 267 | if (config.isUpdateVersion){ 268 | fs.writeFile(args.releaseVersionConfigPath, JSON.stringify(playData), (err) => { 269 | if (err) throw err; 270 | Editor.log("生成CDN所有版本配置文件完成"); 271 | }); 272 | } 273 | 274 | // 生成不同模块的更新配置文件 275 | for(var i = 0; i < mPaths.length; i++){ 276 | var vc = new VersionCdn(mPaths[i], mVersions[i]); 277 | vc.process(); 278 | } 279 | 280 | /** 删除文件夹 */ 281 | function deleteFolderRecursive(path) { 282 | var files = []; 283 | if (fs.existsSync(path)) { 284 | files = fs.readdirSync(path); 285 | files.forEach(function(file, index){ 286 | var curPath = path + "/" + file; 287 | if (fs.statSync(curPath).isDirectory()) { 288 | deleteFolderRecursive(curPath); 289 | } 290 | else { 291 | fs.unlinkSync(curPath); 292 | } 293 | }); 294 | } 295 | }; 296 | 297 | deleteFolderRecursive(path.resolve(args.releaseCdnPath + "src")) 298 | deleteFolderRecursive(path.resolve(args.releaseCdnPath + "res")) 299 | 300 | copyDir(path.resolve(args.releasePath + "src"), path.resolve(args.releaseCdnPath + "src"), function (err) { 301 | if (err) Editor.log("发布到CND的 src 目录出错"); 302 | else Editor.log("发布到CND的 src 目录完成") 303 | }); 304 | 305 | copyDir(path.resolve(args.releasePath + "res"), path.resolve(args.releaseCdnPath + "res"), function (err) { 306 | if (err) Editor.log("发布到CND的 res 目录出错"); 307 | else Editor.log("发布到CND的 res 目录完成"); 308 | }); 309 | } 310 | 311 | 312 | 313 | /*----------------------------------------------------------------------------------------------------*/ 314 | 315 | Editor.Panel.extend({ 316 | ready () { 317 | var realPath = path.join(args.releaseCdnPath); 318 | Editor.log(realPath); 319 | if (!fs.existsSync(realPath)) { 320 | fs.mkdirSync(realPath); 321 | } 322 | releaseVersionInitial(); 323 | }, 324 | }); 325 | -------------------------------------------------------------------------------- /example/assets/script/AssetsManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 资源管理 3 | * Author : donggang 4 | * Create Time : 2016.11.26 5 | * 6 | * 需求: 7 | * 1、版本配置文件对比,提示有新版本 8 | * 2、版本清单文件对比,分析需要更新的资源文件 9 | * 3、批量下载更新的资源文件保存到系统本地存储目录 10 | * 4、批量删除服务器清单文件中没有的资源文件 11 | * 5、更新失败或异常中断时,记录更新状态,下次触发更新时恢复更新进度,不下载已更新的文件 12 | * 6、智能队列更新资源文件,以游戏 fps 数据做为动态计算更新速度(手机性能不够,暂时没必要做) 13 | * 7、版本回退功能,通过查询目录的顺序,把版本版本的更新文件放到查询目录中(思考) 14 | * 8、版本文件摘要验证功能(思考) 15 | * 9、配置文件的数据内存是没有删除的(思考是否需要做处理) 16 | */ 17 | 18 | // 本地缓存关键字 19 | var LOCAL_STORAGE_FOLDER = "game-remote-asset"; // 版本资源目录 20 | var LOCAL_STORAGE_KEY_PROJECT = "update_project"; // 本地缓存清单数据 21 | var LOCAL_STORAGE_KEY_UPDATE_STATE = "update_state"; // 本地缓存更新状态数据(每完成一个资源下载会记录完成数据队列) 22 | var MODULE_PROJECT_MANIFEST_PATH = "version/{0}_project"; // 模块清单路径 23 | 24 | // 事件 25 | game.AssetEvent = {}; 26 | game.AssetEvent.NEW_VERSION = "asset_new_version"; // 已是最新版本 27 | game.AssetEvent.NEW_VERSION_FOUND = "asset_new_version_found"; // 找到新版本 28 | game.AssetEvent.SUCCESS = "asset_success"; // 更新成功 29 | game.AssetEvent.FAILD = "asset_failed"; // 更新失败 30 | game.AssetEvent.PROGRESS = "asset_progress"; // 更新进度 31 | game.AssetEvent.LOCAL_PROJECT_MANIFEST_LOAD_FAIL = "asset_local_project_manifest_load_fail"; // 获取游戏中路安装包中资源清单文件失败 32 | game.AssetEvent.REMOTE_VERSION_MANIFEST_LOAD_FAILD = "asset_remote_version_manifest_load_faild"; // 获取远程版本配置文件失败 33 | game.AssetEvent.REMOTE_PROJECT_MANIFEST_LOAD_FAILD = "asset_remote_project_manifest_load_faild"; // 获取远程更新单清文件失败 34 | game.AssetEvent.NO_NETWORK = "asset_no_network"; // 断网 35 | 36 | // 配置 37 | game.AssetConfig = {}; 38 | game.AssetConfig.debugVersion = false; // 无视版本号测试 39 | game.AssetConfig.debugRes = false; // 无视资源版本对比测试 40 | game.AssetConfig.debugProgress = false; // 打印进度日志 41 | game.AssetConfig.testIp = "172.18.254.56"; // 测试服务器地址 42 | game.AssetConfig.testCdn = "http://" + game.AssetConfig.testIp + "/update/"; // 测试 CDN 服务器地址 43 | 44 | game.AssetConfig.concurrent = 1; // 最大并发更新文件数量(有网络IO和文件IO并载数量在边玩游戏边下载时建议不超过2) 45 | game.AssetConfig.line = "line1"; // 版本线路文件夹,用于更新时优先更新没有用的线路,测试成功后切换热更 46 | 47 | var AssetsDownload = require("AssetsDownload"); 48 | var AssetsManager = cc.Class({ 49 | extends : cc.EventTarget, 50 | 51 | /** 更新进度 */ 52 | getProgress : function(){ 53 | return this._progress; 54 | }, 55 | 56 | /** 57 | * 对比服务器版本信息 58 | * @param appManifestPath(string) 本地清单文件路径 59 | */ 60 | check : function (moduleName) { 61 | if (this._isUpdate == true) { 62 | cc.log("【更新】模块{0}正在更新中".format(moduleName)); 63 | return; 64 | } 65 | 66 | if (cc.sys.isNative) { 67 | this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + LOCAL_STORAGE_FOLDER); 68 | 69 | if (jsb.fileUtils.isDirectoryExist(this._storagePath) == false) { 70 | jsb.fileUtils.createDirectory(this._storagePath); 71 | } 72 | 73 | cc.log("【更新】版本本地存储路径 {0}".format(this._storagePath)); 74 | } 75 | else { 76 | this._storagePath = ""; 77 | } 78 | 79 | this._nocache = game.getLocalTime(); 80 | this._ad = new AssetsDownload(); 81 | 82 | this._moduleName = moduleName; // 模块名 83 | this._moduleManifest = LOCAL_STORAGE_KEY_PROJECT + "_" + moduleName; 84 | this._moduleState = LOCAL_STORAGE_KEY_UPDATE_STATE + "_" + moduleName; 85 | 86 | this._appManifest; // 安装里的清单数据(JSON) 87 | this._localManifest; // 本地存储里的清单数据(JSON) 88 | this._remoteManifest; // 远程更新服务器的清单数据(JSON) 89 | 90 | this._progress = 0; // 更新进度 91 | this._isUpdate = true; // 是否正在更新中 92 | 93 | this._loadLocalManifest(MODULE_PROJECT_MANIFEST_PATH.format(moduleName)); 94 | }, 95 | 96 | /** 开始更新版本 */ 97 | update : function () { 98 | // 获取本地存储更新状态数据,如果有则继续上次的更新,无则下载远程服务器版本清单数据 99 | var tempManifest = cc.sys.localStorage.getItem(this._moduleState); 100 | if (tempManifest == null) { 101 | var complete = function (content) { 102 | // 解析远程资源清单数据 103 | try { 104 | this._remoteManifest = JSON.parse(content); 105 | } 106 | catch(e) { 107 | cc.error("【更新】远程路版本清单数据解析错误"); 108 | } 109 | 110 | // 分析并下载资源 111 | this._downloadAssets(); 112 | }.bind(this); 113 | 114 | var error = function (error) { 115 | cc.log("【更新】获取远程路径为 {0} 的版本清单文件失败".format(this._remoteManifest.remoteManifest)); 116 | 117 | this._isUpdate = false; 118 | 119 | this._dispatchEvent(game.AssetEvent.REMOTE_PROJECT_MANIFEST_LOAD_FAILD); 120 | }.bind(this); 121 | 122 | if (game.config.textUpdate) this._localManifest.server = game.AssetConfig.testCdn; 123 | 124 | var url = this._localManifest.server + game.AssetConfig.line + "/" + this._localManifest.remoteManifest; 125 | game.http.get(this._noCache(url), complete, error); 126 | } 127 | else { 128 | cc.log("【更新】获取上次没更新完的版本清单更新状态"); 129 | 130 | this._remoteManifest = JSON.parse(tempManifest); 131 | 132 | // 分析并下载资源 133 | this._downloadAssets(); 134 | } 135 | }, 136 | 137 | /** 加载本地项目中资源清单数据 */ 138 | _loadLocalManifest: function (appManifestPath) { 139 | // 加载本地项目中资源清单数据 140 | cc.loader.loadRes(appManifestPath, function (error, content) { 141 | if (error) { 142 | cc.log("【更新】获取游戏中路安装包中路径为 {0} 的资源清单文件失败".format(appManifestPath)); 143 | this._dispatchEvent(game.AssetEvent.LOCAL_PROJECT_MANIFEST_LOAD_FAIL); 144 | return; 145 | } 146 | 147 | // 安装包中版本清单数据解析 148 | try { 149 | this._appManifest = JSON.parse(content); 150 | } 151 | catch(e) { 152 | cc.error("【更新】安装包中的版本清单数据解析错误"); 153 | } 154 | 155 | // 获取本地存储中版本清单数据(上次更新成功后的远程清单数据) 156 | var data = cc.sys.localStorage.getItem(this._moduleManifest); 157 | if (data) { 158 | try { 159 | this._localManifest = JSON.parse(data); 160 | } 161 | catch(e) { 162 | cc.error("【更新】本地版本清单数据解析错误"); 163 | } 164 | 165 | // 安装包中的版本高于本地存储版本,则替换本地存储版本数据 166 | if (this._localManifest.version < this._appManifest.version){ 167 | // 删除本地存储中的当前模块的旧的资源 168 | for (var key in this._localManifest.assets) { 169 | var filePath = cc.path.join(this._storagePath, key); 170 | if (jsb.fileUtils.isFileExist(filePath)){ 171 | jsb.fileUtils.removeFile(filePath); 172 | } 173 | } 174 | 175 | cc.log("【更新】安装包的版本号为{0},本地存储版本号为{1},替换本地存储版本数据".format(this._appManifest.version, this._localManifest.version)) 176 | this._localManifest = this._appManifest; 177 | } 178 | } 179 | else { 180 | cc.log("【更新】第一次安装,获取安装版中的版本清单数据"); 181 | this._localManifest = this._appManifest; 182 | } 183 | 184 | // 检查版本号 185 | this._checkVersion(); 186 | }.bind(this)); 187 | }, 188 | 189 | /** 检查版本号 */ 190 | _checkVersion: function () { 191 | var complete = function (content) { 192 | /** 远程版本数据解析 */ 193 | try { 194 | var remoteVersion = JSON.parse(content); 195 | 196 | // 游戏中路资源版本小于远程版本时,提示有更新 197 | if (game.AssetConfig.debugVersion || this._localManifest.version < remoteVersion.version) { 198 | cc.log("【更新】当前版本号为 {0},服务器版本号为 {1}, 有新版本可更新".format(this._appManifest.version, remoteVersion.version)); 199 | this._dispatchEvent(game.AssetEvent.NEW_VERSION_FOUND); // 触发有新版本事件 200 | } 201 | else{ 202 | cc.log("【更新】当前为最新版本"); 203 | this._isUpdate = false; 204 | this._dispatchEvent(game.AssetEvent.NEW_VERSION); // 触发已是最新版本事件 205 | } 206 | } 207 | catch(e) { 208 | cc.error("【更新】远程路版本数据解析错误"); 209 | } 210 | }.bind(this); 211 | 212 | var error = function (error) { 213 | cc.log("【更新】获取远程路径为 {0} 的版本文件失败".format(this._localManifest.remoteVersion)); 214 | this._isUpdate = false; 215 | this._dispatchEvent(game.AssetEvent.REMOTE_VERSION_MANIFEST_LOAD_FAILD); 216 | }.bind(this); 217 | 218 | if (game.config.textUpdate) this._localManifest.server = game.AssetConfig.testCdn; 219 | 220 | // 获取远程版本数据 221 | var url = this._localManifest.server + game.AssetConfig.line + "/" + this._localManifest.remoteVersion; 222 | game.http.get(this._noCache(url), complete, error); 223 | }, 224 | 225 | /** 开始下载资源 */ 226 | _downloadAssets: function () { 227 | // 触发热更进度事件 228 | this._ad.onProgress = function(relativePath, percent){ 229 | this._progress = percent; 230 | 231 | // 记录当前更新状态,更新失败时做为恢复状态使用 232 | this._remoteManifest.assets[relativePath].state = true; 233 | cc.sys.localStorage.setItem(this._moduleState, JSON.stringify(this._remoteManifest)); 234 | 235 | this._dispatchEvent(game.AssetEvent.PROGRESS); 236 | }.bind(this); 237 | 238 | // 触发热更完成事件 239 | this._ad.onComplete = function(){ 240 | this._isUpdate = false; 241 | 242 | // 删除更新状态数据 243 | cc.sys.localStorage.removeItem(this._moduleState); 244 | 245 | // 更新本地版本清单数据,用于下次更新时做版本对比 246 | for (var key in this._remoteManifest.assets) { 247 | var asset = this._remoteManifest.assets[key]; 248 | if (asset.state) delete asset.state; 249 | } 250 | cc.sys.localStorage.setItem(this._moduleManifest, JSON.stringify(this._remoteManifest)); 251 | 252 | // 触发热更完成事件 253 | this._dispatchEvent(game.AssetEvent.SUCCESS); 254 | }.bind(this); 255 | 256 | // 触发热更失败事件 257 | this._ad.onFaild = function(){ 258 | this._isUpdate = false; 259 | this._dispatchEvent(game.AssetEvent.FAILD); 260 | }.bind(this); 261 | 262 | // 触发断网事件 263 | this._ad.onNoNetwork = function(){ 264 | this._isUpdate = false; 265 | this._dispatchEvent(game.AssetEvent.NO_NETWORK); 266 | }.bind(this); 267 | 268 | this._ad.download(this._storagePath, this._localManifest, this._remoteManifest); 269 | }, 270 | 271 | /** 断网后恢复状态 */ 272 | recovery : function(){ 273 | this._ad.recovery(); 274 | }, 275 | 276 | /** 277 | * 触发事件 278 | * @param type(string) 事件类型 279 | * @param args(object) 事件参数 280 | */ 281 | _dispatchEvent : function(type, args){ 282 | var event = new cc.Event.EventCustom(); 283 | event.type = type; 284 | event.bubbles = false; 285 | event.target = this; 286 | event.currentTarget = this; 287 | this.dispatchEvent(event); 288 | }, 289 | 290 | /** 规避 HTTP 缓存问题 */ 291 | _noCache: function (url) { 292 | return url + "?t=" + this._nocache; 293 | } 294 | }) 295 | 296 | /** 验证是否有覆盖安装,安装包中版本高于本地资源时,删除本地资源的模块资源 */ 297 | AssetsManager.check = function(remoteVersion){ 298 | var moduleTotal = 0; 299 | var moduleCurrent = 0; 300 | 301 | // 加载安装包中的版本清单文件 302 | var loadAppManifest = function(moduleName, modules, versions, remoteVersion){ 303 | var appManifestPath = MODULE_PROJECT_MANIFEST_PATH.format(moduleName); 304 | cc.loader.loadRes(appManifestPath, function (error, content) { 305 | if (error) { 306 | cc.error("【更新】验证是否有覆盖安装时,获取游戏中路安装包中路径为 {0} 的资源清单文件失败".format(appManifestPath)); 307 | return; 308 | } 309 | 310 | var storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + LOCAL_STORAGE_FOLDER); 311 | var appManifest = JSON.parse(content); 312 | var appVersion = appManifest.version; 313 | 314 | // 获取本地版本清单信息 315 | var moduleManifest = LOCAL_STORAGE_KEY_PROJECT + "_" + moduleName; 316 | var manifest = cc.sys.localStorage.getItem(moduleManifest); 317 | if (manifest) { 318 | var localManifest = JSON.parse(manifest); 319 | var localVersion = localManifest.version; 320 | 321 | // 安装包中的版本高于本地存储版本,则替换本地存储版本数据 322 | if (localVersion < appVersion){ 323 | // 删除本地存储中的当前模块的旧的资源 324 | for (var key in localManifest.assets) { 325 | var filePath = cc.path.join(storagePath, key); 326 | if (jsb.fileUtils.isFileExist(filePath)){ 327 | jsb.fileUtils.removeFile(filePath); 328 | } 329 | } 330 | 331 | versions[moduleName] = appVersion; // 有本地清单数据时,当前版本号为安装包版本号 332 | modules[moduleName] = modules[moduleName] > appVersion; // 有本地清单数据时,安卓包版本号小于远程版本号 333 | } 334 | else{ 335 | versions[moduleName] = localVersion; // 有本地清单数据时,当前版本号为本地版本号 336 | modules[moduleName] = modules[moduleName] > localVersion; // 有本地清单数据时,本地清单版本号小于远程版本号 337 | } 338 | } 339 | else{ 340 | versions[moduleName] = appVersion; // 没有本地清单数据时,当前版本号为安装包版本号 341 | modules[moduleName] = modules[moduleName] > appVersion; // 没有本地清单数据时,安卓包版本号小于远程版本号 342 | } 343 | 344 | moduleCurrent++; 345 | 346 | if (moduleCurrent == moduleTotal){ 347 | if (remoteVersion) remoteVersion(modules, versions); 348 | } 349 | }); 350 | } 351 | 352 | // 游戏所有模块的配置文件 353 | var url = ""; 354 | 355 | if (game.config.useSSL){ 356 | url = "https://{0}:3001/constinfo/version?t={1}".format(game.config.gateSocketIp, game.getLocalTime()); 357 | } 358 | else{ 359 | url = "http://{0}:3001/constinfo/version?t={1}".format(game.config.gateSocketIp, game.getLocalTime()); 360 | } 361 | 362 | if (game.config.textUpdate) url = "http://{0}:3001/constinfo/version.json?t={1}".format(game.AssetConfig.testIp, game.getLocalTime()); 363 | 364 | // 加载游戏模块当前最前版本号数据 365 | game.http.get(url, function(version_json){ 366 | var json = JSON.parse(version_json); 367 | var modules = json.modules; 368 | var versions = {}; 369 | game.AssetConfig.line = json.line; 370 | 371 | // 计算游戏共有多少个模块 372 | for(var moduleName in modules){ 373 | moduleTotal++; 374 | } 375 | 376 | // 载入游戏所有安装包中的模块版本数据 377 | for(var moduleName in modules){ 378 | loadAppManifest(moduleName, modules, versions, remoteVersion); 379 | } 380 | }.bind(this)); 381 | } 382 | 383 | /** 384 | * 删除模块 385 | * @param moduleName(string) 模块名 386 | */ 387 | AssetsManager.delete = function(moduleName){ 388 | var storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + LOCAL_STORAGE_FOLDER); 389 | var data = cc.sys.localStorage.getItem(LOCAL_STORAGE_KEY_PROJECT + "_" + moduleName); 390 | if (data) { 391 | try { 392 | var localManifest = JSON.parse(data); 393 | 394 | for (var key in localManifest.assets) { 395 | var filePath = cc.path.join(storagePath, key); 396 | if (jsb.fileUtils.isFileExist(filePath)){ 397 | jsb.fileUtils.removeFile(filePath); 398 | } 399 | } 400 | } 401 | catch(e) { 402 | cc.error("【更新】删除模块时,本地版本清单数据解析错误"); 403 | } 404 | 405 | cc.sys.localStorage.removeItem(LOCAL_STORAGE_KEY_PROJECT + "_" + moduleName); 406 | cc.sys.localStorage.removeItem(LOCAL_STORAGE_KEY_UPDATE_STATE + "_" + moduleName); 407 | } 408 | } 409 | 410 | game.asset = module.exports = AssetsManager; 411 | --------------------------------------------------------------------------------