# cocos_hot_update **Repository Path**: longsf/cocos_hot_update ## Basic Information - **Project Name**: cocos_hot_update - **Description**: cocos热更新代码 - **Primary Language**: TypeScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2023-11-03 - **Last Updated**: 2023-11-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 热更新实现 `cocos creator客户端` + `nodejs服务端` 实现手机游戏基础的热更新和自定义热更新。 gitee项目地址: `https://gitee.com/mr_yang_xiaoyi/cocos_hot_update.git` ## 参考文档链接 * `cocos` 官方热更新文档 `https://docs.cocos.com/creator/manual/zh/advanced-topics/hot-update-manager.html` * `cocos` 商店热更新插件使用文档 `https://tidys.gitee.io/doc/#/docs/hot-update-tools/README?id=%e5%b7%a5%e5%85%b7%e8%af%b4%e6%98%8e` * 论坛大佬限定 `cocos 2.4.x` 版本热更新讲解 `https://forum.cocos.org/t/topic/101219` ### 对于热更代码的理解 1. **使用热更新代码需要首先调用 `init` 方法对热更脚本进行初始化** `init` 初始化逻辑中包含以下内容: * 通过调用传参的方式获取游戏项目中的 `manifest` 文件,方便后面 `_assetMgr` 进行加载、读取游戏初始 `version` 、自定义热更修改 `manifest` * 获取当前 `version` ```typescript // 第一步:首先尝试从数据缓存中获取"curVersion"字段对应的数据,因为一旦热更完毕就会在数据缓存中写入最新的version过。如果获取到说明还没有热更过 // 第二步:没有热更过,就从本地manifest文件中获取初始version。从manifest文件中获取version文件的逻辑,如下。 private getCurVersion(): string { // 尝试从缓存中获取 let curVersion = cc.sys.localStorage.getItem(this.strogeKey_curVersion); if (curVersion) return curVersion; let storagePath = this.getRootPath(); storagePath = this.manifestUrl.nativeUrl; if (storagePath) { // 从本地缓存路径读取manifest文件 let loadManifest = jsb.fileUtils.getStringFromFile(storagePath); let manifestObject = JSON.parse(loadManifest); curVersion = manifestObject.version; } return curVersion; } ``` * 初始化 `_assetMgr` 方法实现如下: ```typescript /**初始化 */ public init(manifest: cc.Asset, cb: (curV: string) => void): void { this.isUpdating = false; this.manifestUrl = manifest; this._curVersion = this.getCurVersion(); this._storagePath = this.getRootPath(); this._assetMgr = new jsb.AssetsManager("", this._storagePath, this.versionCompare.bind(this)); this._assetMgr.setVerifyCallback(this.setVerifycb.bind(this)); // 返回当前版本号 cb && cb(this._curVersion); } ``` 2. **进行热更新检测** `checkHotUpdate` 热更新检测方法中包含以下内容: * 由于初始化 `_assetMgr` 时传入的 `manifestUrl` 为空字符串,所以需要把项目中的 `manifest` 通过 `loadLocalManifest` 方法传入。在热更检测的时候拿来和远程 `manifest` 文件进行对比 * 设置热更新检测回调方法,根据返回值 `event` 中的 `Code` 做不同的逻辑处理 * 调用 `checkUpdate` 方法进行热更检测(获取远程 `manifest` 并进行对比并调用回调方法) 方法实现如下: ```typescript /**检测是否能够热更新 */ public checkHotUpdate(): void { if (this.isUpdating) { return; } let url = this.manifestUrl.nativeUrl; console.log("原包版本信息url: ", url); if (this._assetMgr.getState() == jsb.AssetsManager.State.UNINITED) { if (cc.loader.md5Pipe) { url = cc.loader.md5Pipe.transformURL(url); } this._assetMgr.loadLocalManifest(url); } if (!this._assetMgr.getLocalManifest() || !this._assetMgr.getLocalManifest()) { console.log("加载本地manifest文件失败"); return; } console.log("localManifest packageUrl:", this._assetMgr.getLocalManifest().getPackageUrl()); this._assetMgr.setEventCallback(this.checkUpdateEvent.bind(this)); this._assetMgr.checkUpdate(); this.isUpdating = true; } private checkUpdateEvent(event: jsb.EventAssetsManager): void { console.log("checkUpdateEvent Code: %s", event.getEventCode()); switch (event.getEventCode()) { case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST: // this.label.string = "没有本地manifest文件,跳过热更."; this._isNeedUpdateFn && this._isNeedUpdateFn(false, "没有本地manifest文件,跳过热更."); break; case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST: case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST: // this.label.string = "下载远程manifest文件失败,跳过热更."; this._isNeedUpdateFn && this._isNeedUpdateFn(false, "下载远程manifest文件失败,跳过热更."); break; case jsb.EventAssetsManager.ALREADY_UP_TO_DATE: // this.label.string = "已经更新到远程最新版本."; this._isNeedUpdateFn && this._isNeedUpdateFn(false, "已经更新到远程最新版本."); break; case jsb.EventAssetsManager.NEW_VERSION_FOUND: // this.label.string = '发现新版本,请尝试热更'; this._isNeedUpdateFn && this._isNeedUpdateFn(true, "发现新版本,请尝试热更"); break; default: return; } this._assetMgr.setEventCallback(null); this.isUpdating = false; } ``` 3. **热更远程资源** `hotUpdate` 方法包含以下内容: * 设置更新回调方法,同样根据 `event` 中的 `Code` 进行不同的逻辑处理 * 调用 `update` 方法执行热更 * 热更失败根据需要重新执行热更 * 热更成功更新 `searchPaths`、 `HotUpdateSearchPaths` 以及 `curVersion` 等数据 * 热更新进行中获取进度并进行 `ui` 展示 方法实现如下: ```typescript /**热更新 */ public hotUpdate(): void { if (this.isUpdating) return; if (!this._assetMgr) return; this._assetMgr.setEventCallback(this.hotUpdateEvent.bind(this)); if (this._assetMgr.getState() == jsb.AssetsManager.State.UNINITED) { let url = this.manifestUrl.nativeUrl; if (cc.loader.md5Pipe) { url = cc.loader.md5Pipe.transformURL(url); } this._assetMgr.loadLocalManifest(url); } this._assetMgr.update(); this.isUpdating = true; } private hotUpdateEvent(event: jsb.EventAssetsManager): void { console.log("hotUpdateEvent Code: [%s] Msg: [%s]", event.getEventCode(), event.getMessage()); switch (event.getEventCode()) { case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST: // this.label.string = '没有本地manifest文件,跳过热更.'; // failed = true; this.hotUpdateFail(event, "没有本地manifest文件,跳过热更."); break; case jsb.EventAssetsManager.UPDATE_PROGRESSION: console.log("当前下载文件数", event.getDownloadedFiles()) console.log("总文件数", event.getTotalFiles()) // var msg = event.getMessage(); // if (msg) { // this.label.string = '更新的文件:: ' + msg; // } this.hotUpdating(event, '更新的文件:: ' + event.getMessage()); break; case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST: case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST: // this.label.string = '下载远程manifest文件失败,跳过热更.'; // failed = true; this.hotUpdateFail(event, "下载远程manifest文件失败,跳过热更."); break; case jsb.EventAssetsManager.ALREADY_UP_TO_DATE: // this.label.string = '已经更新到远程最新版本.'; // failed = true; this.hotUpdateFail(event, "已经更新到远程最新版本."); break; case jsb.EventAssetsManager.UPDATE_FINISHED: // this.label.string = '更新完成,即将重启游戏. ' + event.getMessage(); // needRestart = true; this.hotUpdateSucc(event, '更新完成,即将重启游戏. ' + event.getMessage()); break; case jsb.EventAssetsManager.UPDATE_FAILED: // this.label.string = '更新失败. ' + event.getMessage(); // this.updating = false; // // this._canRetry = true; this.hotUpdateFail(event, '更新失败. ' + event.getMessage()); break; case jsb.EventAssetsManager.ERROR_UPDATING: // this.label.string = 'Asset 更新错误: ' + event.getAssetId() + ', ' + event.getMessage(); this.hotUpdateFail(event, 'Asset 更新错误: ' + event.getAssetId() + ', ' + event.getMessage()); break; case jsb.EventAssetsManager.ERROR_DECOMPRESS: // this.label.string = event.getMessage(); this.hotUpdateFail(event, event.getMessage()); break; default: break; } } private hotUpdateFail(event: jsb.EventAssetsManager, msg: string): void { console.log("hotUpdateFail Msg: [%s]", msg); this.isUpdating = false; this._updateFailFn && this._updateFailFn(msg); } private hotUpdateSucc(event: jsb.EventAssetsManager, msg: string): void { this._assetMgr.setEventCallback(null); let searchPaths = jsb.fileUtils.getSearchPaths(); let newPaths = this._assetMgr.getLocalManifest().getSearchPaths(); Array.prototype.unshift(searchPaths, newPaths); cc.sys.localStorage.setItem("HotUpdateSearchPaths", JSON.stringify(searchPaths)); jsb.fileUtils.setSearchPaths(searchPaths); let remoteVersion = this.getRemoteVersion(); cc.sys.localStorage.setItem(this.strogeKey_curVersion, remoteVersion); this._updateSuccFn && this._updateSuccFn(remoteVersion); } private hotUpdating(event: jsb.EventAssetsManager, msg: string): void { let count_Download = event.getDownloadedFiles(); let count_Total = event.getTotalFiles(); this._updateProgressFn && this._updateProgressFn(count_Download / count_Total, msg); } private getRemoteVersion(): string { let storagePath = this.getRootPath(); console.log("有下载的manifest文件", storagePath); let loadManifest = jsb.fileUtils.getStringFromFile(storagePath + "/project.manifest"); let manifestObject = JSON.parse(loadManifest); return manifestObject.version; } ``` 4. **自定义热更新** `customerHotUpdate` 方法包含以下内容: * 从后端获取自定义更新到的游戏版本,拿到对应下载地址( `data->tarPath` ) * 进行比较判断是否需要进行热更 * 改变本地缓存中的 `manifest` 文件中 `packageUrl` 、 `remoteManifestUrl` 、 `remoteVersionUrl` 字段对应的值 * 然后执行正常的热更逻辑 方法实现如下: ```typescript /**自定义热更新 */ public customerHotUpdate(): void { let curV = this.getCurVersion(); this.reqHotUpdateInfo(curV, (info: { curV: string, tarV: string, tarPath: string, curPath: string }) => { /* // 大版本更新 1.x.x -> 2.x.x 需要重新下载并安装apk let rootpath = this.getRootPath(); // 清除之前缓存的版本号 和 版本路径 cc.sys.localStorage.removeItem(this.strogeKey_curVersion); jsb.fileUtils.removeDirectory(rootpath); // 打开新版本应用下载页 let packageUrl = ""; cc.sys.openURL(packageUrl); */ // 小版本更新 let needUpdate = this.versionCompare(info.curV, info.tarV); if (needUpdate == 0) { console.log("小版本相同,不用更新"); return; } this._modifyAppLoadUrlForManifestFile(info.tarPath, this.manifestUrl.nativeUrl); }); } /** * 修改.manifest文件,如果有缓存则比较缓存的版本和应用包的版本,取较高者 热更,如果没有缓存则下载远程的版本文件 取版本号,这里统一修改地址为 * 远程地址,不管 如何变化 都是从最新的地址下载版本文件。这里远程 * @param {新的升级包地址} newAppHotUpdateUrl * @param {本地project.manifest文件地址} localManifestPath */ private _modifyAppLoadUrlForManifestFile(newAppHotUpdateUrl, localManifestPath) { let isWritten = false; if (jsb.fileUtils.isFileExist(this.getRootPath() + '/project.manifest')) { let storagePath = this.getRootPath(); console.log("有下载的manifest文件", storagePath); let loadManifest = jsb.fileUtils.getStringFromFile(storagePath + '/project.manifest'); let manifestObject = JSON.parse(loadManifest); manifestObject.packageUrl = newAppHotUpdateUrl; manifestObject.remoteManifestUrl = newAppHotUpdateUrl + "/project.manifest"; manifestObject.remoteVersionUrl = newAppHotUpdateUrl + "/version.manifest"; let afterString = JSON.stringify(manifestObject); isWritten = jsb.fileUtils.writeStringToFile(afterString, storagePath + '/project.manifest'); } else { /** * 执行到这里说明App之前没有进行过热更,所以不存在热更的plane文件夹。 */ /** * plane文件夹不存在的时候,我们就主动创建“plane”文件夹,并将打包时候的project.manifest文件中升级包地址修改后,存放到“plane”文件夹下面。 */ let initializedManifestPath = this.getRootPath(); if (!jsb.fileUtils.isDirectoryExist(initializedManifestPath)) { jsb.fileUtils.createDirectory(initializedManifestPath); } //修改原始manifest文件 let originManifestPath = localManifestPath; let originManifest = jsb.fileUtils.getStringFromFile(originManifestPath); let originManifestObject = JSON.parse(originManifest); originManifestObject.packageUrl = newAppHotUpdateUrl; originManifestObject.remoteManifestUrl = newAppHotUpdateUrl + '/project.manifest'; originManifestObject.remoteVersionUrl = newAppHotUpdateUrl + '/version.manifest'; let afterString = JSON.stringify(originManifestObject); isWritten = jsb.fileUtils.writeStringToFile(afterString, initializedManifestPath + '/project.manifest'); } cc.log("Written Status : ", isWritten); if (isWritten) { this.checkHotUpdate(); } } public reqHotUpdateInfo(curV: string, cb: Function): void { let xhr = cc.loader.getXMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { let txt = xhr.responseText; console.log("get hotupdate info from server %s", txt); let data = JSON.parse(txt); cb && cb(data); } }; xhr.withCredentials = false; xhr.open("GET", "http://192.168.43.252:3000/hotUpdate?curV=" + curV); xhr.send(); } ``` 通过学习大佬代码知道自己对热更新实现的欠缺: * 本地 `manifest` 文件,获取 `version` 方式 ```typescript var manifest: cc.Asset // 项目中获取到的manifest文件,在这里默认能够获取到 var storagePath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : "/") + "remote-asset"; // 后面remote-asset可以自己定义改写 var version = ""; // 第一种获取方式 实例化jsb.AssetManager将manifest文件url传入,通过mgr获取manifest文件中的信息 let mgr = new jsb.AssetManager(manifest, storagePath, null); version = mgr.getLocalManifest().getVersion(); // 第二种获取方式 通过manifest文件获取存储路径,然后通过jsb.fileUtils读取并以string的形式初始,经过JSON转换之后就可以获取到version let url = manifest.nativeUrl; let manifestStr = jsb.fileUtils.getStringFromFile(storagePath); let manifestObj = JSON.parse(manifestStr); version = manifestObj.version; ``` * 使用 `jsb.fileUtil` 对游戏内文件和缓存文件的创建、读取操作 * 从服务器获取想要更新到的游戏版本路径,然后对本地热更缓存文件进行创建写入、修改现有缓存文件达到更新游戏到指定版本的目的