热更新工具修改

This commit is contained in:
gongxh 2025-04-18 17:51:42 +08:00
parent 80558e51fe
commit b7a35b05c0
7 changed files with 353 additions and 171 deletions

View File

@ -4,11 +4,52 @@
* @Description:
*/
import { Asset, game, native, sys } from "cc";
import { game, native, sys } from "cc";
import { Platform } from "../global/Platform";
import { log, warn } from "../tool/log";
import { ICheckUpdatePromiseResult, IPromiseResult } from "../interface/PromiseResult";
import { ReadNetFile } from "../net/nettools/ReadNetFile";
import { debug, log } from "../tool/log";
import { Utils } from "../tool/Utils";
interface IHotUpdateConfig {
packageUrl: string;
remoteManifestUrl: string;
remoteVersionUrl: string;
}
export enum HotUpdateCode {
/** 成功 */
SUCCEED = 0,
/** 重复初始化 */
REPEAT_INIT = -1001,
/** 平台不支持 不需要热更新 */
PLATFORM_NOT_SUPPORTED = -1002,
/** 未初始化 */
NOT_INITIALIZED = -1003,
/** 更新中 */
UPDATING = -1004,
/** 加载本地manifest失败 */
LOAD_LOCAL_MANIFEST_FAILED = -1005,
/** 下载manifest文件失败 */
DOWNLOAD_MANIFEST_FAILED = -1006,
/** 解析manifest文件失败 */
PARSE_MANIFEST_FAILED = -1007,
/** 更新失败 需要重试 */
UPDATE_FAILED = -1008,
/** 更新错误 */
UPDATE_ERROR = -1009,
/** 解压错误 */
DECOMPRESS_ERROR = -1010,
/** 下载 hotconfig 文件失败 */
DOWNLOAD_HOTCONFIG_FAILED = -1011,
/** 解析 hotconfig 文件失败 */
PARSE_HOTCONFIG_FAILED = -1012,
}
const TAG = "hotupdate:";
export class HotUpdateManager {
@ -19,7 +60,10 @@ export class HotUpdateManager {
}
return HotUpdateManager.instance;
}
/** 本地manifest路径 */
private _manifestUrl: string = '';
/** 热更新配置路径 (可选) 用来动态替换manifest文件中关联的地址 */
private _hotconfigUrl: string = '';
/** 版本号 */
private _version: string = '';
/** 可写路径 */
@ -29,27 +73,35 @@ export class HotUpdateManager {
/** 是否正在更新 或者 正在检查更新 */
private _updating: boolean = false;
/** 检查更新的回调 */
private _checkSucceed: (need: boolean, size: number) => void = null;
private _checkFail: (code: number, message: string) => void = null;
/** 更新回调 */
private _updateProgress: (kb: number, total: number) => void = null;
private _updateFail: (code: number, message: string) => void = null;
private _updateError: (code: number, message: string) => void = null;
private _updateProgress: (kb: number, total: number) => void = null; // 更新进度
private _updateFail: (res: IPromiseResult) => void = null; // 更新失败
private _finish: (res: IPromiseResult) => void = null; // 更新结束
/**
* , 使
* @return 0
*/
public get resVersion(): string {
return this._am?.getLocalManifest()?.getVersion() || "0";
}
/**
* 1.
* @param manifest manifest文件
* @param manifestUrl manifest文件地址 assets.nativeUrl
* @param version eg: 1.0.0
* @param hotconfigUrl manifest文件中关联的地址
*/
public init(manifest: Asset, version: string): void {
public init(manifestUrl: string, version: string, hotconfigUrl?: string): Promise<IPromiseResult> {
return new Promise((resolve, reject) => {
if (!Platform.isNativeMobile) {
return;
reject({ code: HotUpdateCode.PLATFORM_NOT_SUPPORTED, message: "当前平台不支持热更新" });
}
if (this._am) {
warn(`${TAG}请勿重复初始化`);
return;
reject({ code: HotUpdateCode.REPEAT_INIT, message: "请勿重复初始化" });
}
this._manifestUrl = manifestUrl;
this._hotconfigUrl = hotconfigUrl || '';
this._version = version;
let writablePath = native?.fileUtils?.getWritablePath() || "";
@ -64,145 +116,186 @@ export class HotUpdateManager {
this._am.setVersionCompareHandle(Utils.compareVersion);
this._am.setVerifyCallback(this._verifyCallback);
// 加载本地的 manifest
log(`${TAG} 加载本地的 manifest:${manifest.nativeUrl}`);
this._am.loadLocalManifest(manifest.nativeUrl);
log(`${TAG} 加载本地的 manifest:${manifestUrl}`);
if (this._am.loadLocalManifest(manifestUrl)) {
resolve({ code: HotUpdateCode.SUCCEED, message: "初始化成功" });
} else {
reject({ code: HotUpdateCode.LOAD_LOCAL_MANIFEST_FAILED, message: "加载本地manifest失败" });
}
});
}
/** 加载远程的hotconfig文件 并替换local manifest内容 */
private loadRemoteHotConfig(): Promise<IPromiseResult> {
return new Promise((resolve, reject) => {
new ReadNetFile({
url: this._hotconfigUrl,
timeout: 5,
responseType: "text",
onComplete: (data: string) => {
log(`${TAG} 下载hotconfig文件成功`);
if (Utils.isJsonString(data)) {
let hotconfig: IHotUpdateConfig = JSON.parse(data);
let content = native.fileUtils.getStringFromFile(this._manifestUrl); // 这个一定有
// 使用拿到的cdn上的配置修改本地manifest内容
let newManifest: IHotUpdateConfig = JSON.parse(content);
newManifest.remoteManifestUrl = hotconfig.remoteManifestUrl;
newManifest.remoteVersionUrl = hotconfig.remoteVersionUrl;
newManifest.packageUrl = hotconfig.packageUrl;
// 注册本地manifest根目录
let manifestRoot = "";
let found = this._manifestUrl.lastIndexOf("/");
if (found === -1) {
found = this._manifestUrl.lastIndexOf("\\");
}
if (found !== -1) {
manifestRoot = this._manifestUrl.substring(0, found + 1);
}
this._am.getLocalManifest().parseJSONString(JSON.stringify(newManifest), manifestRoot);
// log(TAG + "manifest root:" + this._am.getLocalManifest().getManifestRoot());
// log(TAG + "manifest packageUrl:" + this._am.getLocalManifest().getPackageUrl());
// log(TAG + "manifest version:" + this._am.getLocalManifest().getVersion());
// log(TAG + "manifest versionFileUrl:" + this._am.getLocalManifest().getVersionFileUrl());
// log(TAG + "manifest manifestFileUrl:" + this._am.getLocalManifest().getManifestFileUrl());
// 重新加载本地manifest
resolve({ code: HotUpdateCode.SUCCEED, message: "更新热更新配置成功" });
} else {
log(`${TAG} hotconfig.manifest文件格式错误`);
resolve({ code: HotUpdateCode.PARSE_HOTCONFIG_FAILED, message: "hotconfig.manifest文件格式错误" });
}
},
onError: (code: number, message: string) => {
log(`${TAG} 下载hotconfig.manifest文件失败`, code, message);
// 下载hotconfig.manifest文件失败
resolve({ code: HotUpdateCode.DOWNLOAD_HOTCONFIG_FAILED, message: "下载hotconfig.manifest文件失败" });
}
});
})
}
/**
* 2.
* @param res.succeed.need
* @param res.succeed.size (KB)
*
* @param res.fail
* @param res.fail.code
* -1000: 未初始化
* -1001: 正在更新或者正在检查更新
* -1002: 本地manifest文件错误
* -1004: 解析远程manifest文件失败
*
*
* @return {Promise<ICheckUpdatePromiseResult>}
*/
public checkUpdate(res: { succeed: (need: boolean, size: number) => void, fail: (code: number, message: string) => void }): void {
this._checkSucceed = res.succeed;
this._checkFail = res.fail;
if (this._updating) {
res.fail(-1001, "正在更新或者正在检查更新");
return;
}
public checkUpdate(): Promise<ICheckUpdatePromiseResult> {
return new Promise((resolve, reject) => {
if (!Platform.isNativeMobile) {
res.succeed(false, 0);
reject({ code: HotUpdateCode.PLATFORM_NOT_SUPPORTED, message: "当前平台不需要热更新" });
return;
}
if (!this._am) {
res.fail(-1000, "未初始化, 需要先调用init方法");
reject({ code: HotUpdateCode.NOT_INITIALIZED, message: "未初始化, 需要先调用init方法" });
return;
}
if (this._updating) {
reject({ code: HotUpdateCode.UPDATING, message: "正在更新或者正在检查更新" });
return;
}
this._updating = true;
// 设置回调
this._am.setEventCallback(this._checkCb.bind(this));
// 检查更新
this._am.checkUpdate();
}
/**
* 3.
* @param res.progress kb: 已下载的资源大小, total: 总资源大小 (kb)
* @param res.fail
* @param res.fail.code
* -10001: 更新失败
* @param res.error
* @param res.error.code
* -1000: 未初始化
* -1001: 正在更新或者正在检查更新
* -10002: 资源更新错误
* -10003: 解压错误
*/
public startUpdate(res: {
progress: (kb: number, total: number) => void,
fail: (code: number, message: string) => void,
error: (code: number, message: string) => void
}): void {
this._updateProgress = res.progress;
this._updateFail = res.fail;
this._updateError = res.error;
log(`${TAG} 开始热更新`);
if (this._updating) {
res.error(-1001, "正在更新或者正在检查更新");
return;
}
if (!this._am) {
res.error(-1000, "未初始化, 需要先调用init方法");
return;
}
this._updating = true;
this._am.setEventCallback(this._updateCb.bind(this));
this._am.update();
}
/** 重试失败的资源 */
public retryUpdate(): void {
this._am.downloadFailedAssets();
}
/** 检查更新的回调 */
private _checkCb(event: native.EventAssetsManager) {
this._am.setEventCallback((event: native.EventAssetsManager) => {
let eventCode = event.getEventCode();
log(`${TAG} 检查更新回调code:${eventCode}`);
this._updating = false;
switch (eventCode) {
case native.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this._checkFail(-1002, "本地没有manifest文件");
break;
case native.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
// this._checkFail(-1003, "下载manifest文件失败");
this._checkSucceed(false, 0);
break;
this._am.setEventCallback(null);
this._updating = false;
resolve({ code: HotUpdateCode.DOWNLOAD_MANIFEST_FAILED, message: "下载manifest文件失败", needUpdate: false, size: 0 });
return;
case native.EventAssetsManager.ERROR_PARSE_MANIFEST:
this._checkFail(-1004, "解析远程manifest文件失败");
break;
this._am.setEventCallback(null);
this._updating = false;
resolve({ code: HotUpdateCode.PARSE_MANIFEST_FAILED, message: "解析manifest文件失败", needUpdate: false, size: 0 });
return;
case native.EventAssetsManager.ALREADY_UP_TO_DATE:
this._checkSucceed(false, 0);
break;
this._am.setEventCallback(null);
this._updating = false;
resolve({ code: HotUpdateCode.SUCCEED, message: "已是最新版本", needUpdate: false, size: 0 });
return;
case native.EventAssetsManager.NEW_VERSION_FOUND:
// 发现新版本
this._checkSucceed(true, this._am.getTotalBytes() / 1024);
break;
default:
this._am.setEventCallback(null);
this._updating = false;
resolve({ code: HotUpdateCode.SUCCEED, message: "发现新版本", needUpdate: true, size: this._am.getTotalBytes() / 1024 });
return;
}
this._am.setEventCallback(null);
});
// 如果存在替换地址
if (this._hotconfigUrl) {
log(`${TAG} 下载 hotconfig 文件`);
this.loadRemoteHotConfig().then((res) => {
if (res.code === HotUpdateCode.SUCCEED || res.code === HotUpdateCode.DOWNLOAD_HOTCONFIG_FAILED) {
this._am.checkUpdate();
} else {
this._updating = false;
reject(res);
}
});
} else {
// 使用本地的menifest检查更新
log(`${TAG} 使用本地的menifest检查更新`);
this._am.checkUpdate();
}
});
}
/** 更新的回调 */
private _updateCb(event: native.EventAssetsManager) {
/**
*
* @param res.progress kb: 已下载的资源大小, total: 总资源大小 (kb)
* @param res.fail
* @param res.finish code
*/
public startUpdate(res: {
progress: (kb: number, total: number) => void,
fail: (res: IPromiseResult) => void,
finish: (res: IPromiseResult) => void
}): void {
if (!Platform.isNativeMobile) {
res.finish({ code: HotUpdateCode.PLATFORM_NOT_SUPPORTED, message: "当前平台不需要热更新" });
}
if (!this._am) {
res.finish({ code: HotUpdateCode.NOT_INITIALIZED, message: "未初始化, 需要先调用init方法" });
return;
}
if (this._updating) {
res.finish({ code: HotUpdateCode.UPDATING, message: "正在更新或者正在检查更新" });
return;
}
this._updateProgress = res.progress;
this._updateFail = res.fail;
this._finish = res.finish;
log(`${TAG} 开始热更新`);
this.checkUpdate().then((result) => {
if (result.needUpdate) {
this.update();
} else {
res.finish({ code: HotUpdateCode.SUCCEED, message: "已是最新版本" });
}
}).catch((err) => {
res.finish({ code: err.code, message: err.message });
});
}
private update(): void {
this._updating = true;
this._am.setEventCallback((event: native.EventAssetsManager) => {
let eventCode = event.getEventCode();
log(`${TAG} 更新回调code:${eventCode}`);
let needRestart = false;
debug(`${TAG} 更新回调code:${eventCode}`);
switch (eventCode) {
case native.EventAssetsManager.UPDATE_PROGRESSION:
case native.EventAssetsManager.UPDATE_PROGRESSION: {
let bytes = event.getDownloadedBytes() / 1024;
let total = event.getTotalBytes() / 1024;
this._updateProgress(bytes, total);
break;
case native.EventAssetsManager.UPDATE_FINISHED:
// 更新完成 自动重启
needRestart = true;
break;
case native.EventAssetsManager.UPDATE_FAILED:
this._updating = false;
this._updateFail(-10001, event.getMessage());
break;
case native.EventAssetsManager.ERROR_UPDATING:
this._updating = false;
this._updateError(-10002, event.getMessage());
break;
case native.EventAssetsManager.ERROR_DECOMPRESS:
this._updating = false;
this._updateError(-10003, event.getMessage());
break;
default:
break;
}
if (needRestart) {
case native.EventAssetsManager.UPDATE_FINISHED: {
this._updating = false;
// 更新完成 自动重启
this._am.setEventCallback(null);
// Prepend the manifest's search path
@ -219,11 +312,38 @@ export class HotUpdateManager {
// 重启游戏
setTimeout(() => {
game.restart()
game.restart();
}, 500);
break;
}
case native.EventAssetsManager.UPDATE_FAILED: {
this._updating = false;
this._updateFail({ code: HotUpdateCode.UPDATE_FAILED, message: event.getMessage() });
break;
}
case native.EventAssetsManager.ERROR_UPDATING: {
this._updating = false;
this._finish({ code: HotUpdateCode.UPDATE_ERROR, message: event.getMessage() });
break;
}
case native.EventAssetsManager.ERROR_DECOMPRESS: {
this._updating = false;
this._finish({ code: HotUpdateCode.DECOMPRESS_ERROR, message: event.getMessage() });
break;
}
default:
break;
}
});
this._am.update();
}
/** 重试失败的资源 */
public retryUpdate(): void {
this._am.downloadFailedAssets();
}
/** 验证资源 */
private _verifyCallback(path: string, asset: native.ManifestAsset): boolean {
// 资源是否被压缩, 如果压缩我们不需要检查它的md5值
let compressed = asset.compressed;

View File

@ -0,0 +1,19 @@
/**
* @Author: Gongxh
* @Date: 2025-04-18
* @Description: Promise
*/
export interface IPromiseResult {
/** 0:成功 其他:失败 */
code: number;
/** 失败信息 */
message: string;
}
export interface ICheckUpdatePromiseResult extends IPromiseResult {
/** 是否需要更新 */
needUpdate?: boolean;
/** 需要更新的资源大小 (KB) */
size?: number;
}

View File

@ -4,6 +4,7 @@ export { GlobalTimer } from "./global/GlobalTimer";
export { enableDebugMode, FrameConfig, KUNPO_DEBUG } from "./global/header";
export { Platform, PlatformType } from "./global/Platform";
export { Screen } from "./global/Screen";
export * from "./interface/PromiseResult";
/** tool */
export { Binary } from "./tool/Binary";
@ -24,6 +25,9 @@ export { IHttpResponse } from "./net/http/IHttpResponse";
/** Socket */
export { Socket } from "./net/socket/Socket";
/** 读取网络文件 */
export { ReadNetFile } from "./net/nettools/ReadNetFile";
/** 四叉树 */
export { Box } from "./quadtree/Box";
export { Circle } from "./quadtree/Circle";
@ -78,7 +82,7 @@ export { ConditionAnyNode } from "./condition/node/ConditionAnyNode";
export { ConditionBase } from "./condition/node/ConditionBase";
/** 热更新 */
export { HotUpdateManager } from "./hotupdate/HotUpdateManager";
export { HotUpdateCode, HotUpdateManager } from "./hotupdate/HotUpdateManager";
/** 小游戏 */
export { AlipayCommon } from "./minigame/alipay/AlipayCommon";

View File

@ -4,7 +4,6 @@
* @Description:
*/
import { LaunchParams } from "@douyin-microapp/typings/types/app";
import { warn } from "../../tool/log";
import { IMiniCommon } from "../interface/IMiniCommon";
@ -24,14 +23,14 @@ export class BytedanceCommon implements IMiniCommon {
/**
*
*/
public getLaunchOptions(): LaunchParams {
public getLaunchOptions(): BytedanceMiniprogram.LaunchParams {
return this._launchOptions;
}
/**
*
*/
public getHotLaunchOptions(): LaunchParams {
public getHotLaunchOptions(): BytedanceMiniprogram.LaunchParams {
warn("字节跳动小游戏未提供热启动参数获取方式,请在 onShow 中获取");
return null;
}

View File

@ -8,7 +8,7 @@ import { IHttpResponse } from "./IHttpResponse";
export interface IHttpEvent {
/** 名称 */
name: string;
name?: string;
/** 自定义参数 */
data?: any;
/** 网络请求成功 */

View File

@ -0,0 +1,29 @@
/**
* @Author: Gongxh
* @Date: 2025-04-18
* @Description:
*/
import { Time } from "../../tool/Time";
import { HttpManager } from "../http/HttpManager";
import { IHttpResponse } from "../http/IHttpResponse";
export class ReadNetFile {
constructor(res: { url: string, timeout: number, responseType: "text" | "json" | "arraybuffer", onComplete: (data: any) => void, onError: (code: number, message: string) => void }) {
// 地址上带时间戳参数 确保每次请求都到服务器上请求最新配置,而不是拿到上次请求的缓存数据
let url = res.url;
if (url.indexOf("?") > -1) {
url += `&timeStamp=${Time.now()}`;
} else {
url += `?timeStamp=${Time.now()}`;
}
HttpManager.get(url, null, res.responseType, {
onComplete: (response: IHttpResponse) => {
res.onComplete(response.data);
},
onError: (response: IHttpResponse) => {
res.onError(response.statusCode, response.message);
}
}, null, res.timeout || 6);
}
}

View File

@ -36,4 +36,15 @@ export class Utils {
return 0;
}
/**
* json格式的字符串
*/
public static isJsonString(str: string): boolean {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
}