mirror of
https://github.com/Gongxh0901/kunpolibrary
synced 2025-12-05 20:29:10 +00:00
413 lines
12 KiB
Plaintext
413 lines
12 KiB
Plaintext
---
|
|
description: "热更新系统开发规范"
|
|
globs: ["src/hotupdate/**/*.ts"]
|
|
alwaysApply: false
|
|
type: "system-module"
|
|
---
|
|
|
|
# 热更新系统开发规范
|
|
|
|
## 热更新架构设计
|
|
|
|
### 管理器单例模式
|
|
```typescript
|
|
export class HotUpdateManager {
|
|
private static _instance: HotUpdateManager;
|
|
|
|
/** 获取单例实例 */
|
|
public static getInstance(): HotUpdateManager {
|
|
if (!this._instance) {
|
|
this._instance = new HotUpdateManager();
|
|
}
|
|
return this._instance;
|
|
}
|
|
|
|
/** 禁用直接构造 */
|
|
private constructor() {}
|
|
|
|
/** 配置属性 */
|
|
public manifestUrl: string = "";
|
|
public versionUrl: string = "";
|
|
|
|
/** 初始化热更新 */
|
|
public init(manifestUrl: string, versionUrl: string): void {
|
|
this.manifestUrl = manifestUrl;
|
|
this.versionUrl = versionUrl;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 热更新状态码定义
|
|
```typescript
|
|
export enum HotUpdateCode {
|
|
/** 成功 */
|
|
Succeed = 0,
|
|
/** 已是最新版本 */
|
|
LatestVersion = 1,
|
|
/** 检查更新失败 */
|
|
CheckFailed = 2,
|
|
/** 下载失败 */
|
|
DownloadFailed = 3,
|
|
/** 解压失败 */
|
|
UnzipFailed = 4,
|
|
/** 网络错误 */
|
|
NetworkError = 5,
|
|
/** 空间不足 */
|
|
NoSpace = 6,
|
|
/** 未知错误 */
|
|
UnknownError = 7
|
|
}
|
|
```
|
|
|
|
## Promise 结果模式
|
|
|
|
### 统一结果接口
|
|
```typescript
|
|
export interface IPromiseResult {
|
|
/** 状态码 */
|
|
code: HotUpdateCode;
|
|
|
|
/** 消息描述 */
|
|
message: string;
|
|
|
|
/** 扩展数据 */
|
|
data?: any;
|
|
}
|
|
```
|
|
|
|
### 热更新配置接口
|
|
```typescript
|
|
export interface IHotUpdateConfig {
|
|
/** 版本号 */
|
|
version: string;
|
|
|
|
/** 远程manifest文件URL */
|
|
remoteManifestUrl: string;
|
|
|
|
/** 远程version文件URL */
|
|
remoteVersionUrl: string;
|
|
|
|
/** 资源包URL */
|
|
packageUrl: string;
|
|
|
|
/** 资源文件列表 */
|
|
assets?: { [key: string]: any };
|
|
|
|
/** 搜索路径 */
|
|
searchPaths?: string[];
|
|
}
|
|
```
|
|
|
|
## 热更新核心实现
|
|
|
|
### HotUpdate 核心类
|
|
```typescript
|
|
export class HotUpdate {
|
|
private _am: jsb.AssetsManager;
|
|
private _updating: boolean = false;
|
|
|
|
constructor(manifestUrl: string) {
|
|
// 初始化 AssetsManager
|
|
this._am = new jsb.AssetsManager(manifestUrl, jsb.fileUtils.getWritablePath() + 'remote-assets');
|
|
this._am.setEventCallback(this.onUpdateEvent.bind(this));
|
|
this._am.setVerifyCallback(this.onVerifyCallback.bind(this));
|
|
}
|
|
|
|
/**
|
|
* 检查更新
|
|
* @returns Promise<IPromiseResult>
|
|
*/
|
|
public checkUpdate(): Promise<IPromiseResult> {
|
|
return new Promise((resolve) => {
|
|
if (this._updating) {
|
|
resolve({ code: HotUpdateCode.UnknownError, message: "正在更新中" });
|
|
return;
|
|
}
|
|
|
|
if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
|
|
resolve({ code: HotUpdateCode.CheckFailed, message: "本地manifest加载失败" });
|
|
return;
|
|
}
|
|
|
|
this._am.setEventCallback((event) => {
|
|
switch (event.getEventCode()) {
|
|
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
|
|
resolve({ code: HotUpdateCode.CheckFailed, message: "本地manifest不存在" });
|
|
break;
|
|
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
|
|
resolve({ code: HotUpdateCode.NetworkError, message: "下载manifest失败" });
|
|
break;
|
|
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
|
|
resolve({ code: HotUpdateCode.LatestVersion, message: "已是最新版本" });
|
|
break;
|
|
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
|
|
resolve({ code: HotUpdateCode.Succeed, message: "发现新版本" });
|
|
break;
|
|
}
|
|
});
|
|
|
|
this._am.checkUpdate();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 执行热更新
|
|
* @returns Promise<IPromiseResult>
|
|
*/
|
|
public hotUpdate(): Promise<IPromiseResult> {
|
|
return new Promise((resolve) => {
|
|
if (this._updating) {
|
|
resolve({ code: HotUpdateCode.UnknownError, message: "正在更新中" });
|
|
return;
|
|
}
|
|
|
|
this._updating = true;
|
|
|
|
this._am.setEventCallback((event) => {
|
|
switch (event.getEventCode()) {
|
|
case jsb.EventAssetsManager.UPDATE_FINISHED:
|
|
this._updating = false;
|
|
resolve({ code: HotUpdateCode.Succeed, message: "更新完成" });
|
|
break;
|
|
case jsb.EventAssetsManager.UPDATE_FAILED:
|
|
this._updating = false;
|
|
resolve({ code: HotUpdateCode.DownloadFailed, message: "更新失败" });
|
|
break;
|
|
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
|
|
this._updating = false;
|
|
resolve({ code: HotUpdateCode.UnzipFailed, message: "解压失败" });
|
|
break;
|
|
}
|
|
});
|
|
|
|
this._am.update();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 事件回调处理
|
|
*/
|
|
private onUpdateEvent(event: jsb.EventAssetsManager): void {
|
|
const code = event.getEventCode();
|
|
debug(`热更新事件: ${code}`);
|
|
|
|
switch (code) {
|
|
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
|
|
const progress = event.getPercent();
|
|
debug(`更新进度: ${progress}%`);
|
|
break;
|
|
case jsb.EventAssetsManager.ASSET_UPDATED:
|
|
const assetId = event.getAssetId();
|
|
debug(`资源更新: ${assetId}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 验证回调
|
|
*/
|
|
private onVerifyCallback(path: string, asset: any): boolean {
|
|
// 资源验证逻辑
|
|
return true;
|
|
}
|
|
}
|
|
```
|
|
|
|
## manifest 管理
|
|
|
|
### 本地 manifest 刷新
|
|
```typescript
|
|
/**
|
|
* 替换 project.manifest 中的内容并刷新本地manifest
|
|
*/
|
|
private refreshLocalManifest(manifest: IHotUpdateConfig, versionManifest: IHotUpdateConfig): Promise<IPromiseResult> {
|
|
return new Promise((resolve) => {
|
|
// 版本比较
|
|
if (Utils.compareVersion(manifest.version, versionManifest.version) >= 0) {
|
|
resolve({ code: HotUpdateCode.LatestVersion, message: "已是最新版本" });
|
|
return;
|
|
}
|
|
|
|
// 更新 manifest 配置
|
|
manifest.remoteManifestUrl = Utils.addUrlParam(versionManifest.remoteManifestUrl, "timeStamp", `${Time.now()}`);
|
|
manifest.remoteVersionUrl = Utils.addUrlParam(versionManifest.remoteVersionUrl, "timeStamp", `${Time.now()}`);
|
|
manifest.packageUrl = versionManifest.packageUrl;
|
|
|
|
// 计算 manifest 根目录
|
|
let manifestRoot = "";
|
|
let manifestUrl = HotUpdateManager.getInstance().manifestUrl;
|
|
let found = manifestUrl.lastIndexOf("/");
|
|
if (found === -1) {
|
|
found = manifestUrl.lastIndexOf("\\");
|
|
}
|
|
if (found !== -1) {
|
|
manifestRoot = manifestUrl.substring(0, found + 1);
|
|
}
|
|
|
|
// 解析并设置本地 manifest
|
|
this._am.getLocalManifest().parseJSONString(JSON.stringify(manifest), manifestRoot);
|
|
|
|
resolve({ code: HotUpdateCode.Succeed, message: "更新热更新配置成功" });
|
|
});
|
|
}
|
|
```
|
|
|
|
## 版本比较工具
|
|
|
|
### 版本号比较函数
|
|
```typescript
|
|
export class Utils {
|
|
/**
|
|
* 版本号比较
|
|
* @param versionA 版本A
|
|
* @param versionB 版本B
|
|
* @returns 0: 相等, 1: A > B, -1: A < B
|
|
*/
|
|
public static compareVersion(versionA: string, versionB: string): number {
|
|
const a = versionA.split('.');
|
|
const b = versionB.split('.');
|
|
|
|
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
const numA = parseInt(a[i] || '0', 10);
|
|
const numB = parseInt(b[i] || '0', 10);
|
|
|
|
if (numA > numB) return 1;
|
|
if (numA < numB) return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* 给URL添加参数
|
|
*/
|
|
public static addUrlParam(url: string, key: string, value: string): string {
|
|
const separator = url.indexOf('?') !== -1 ? '&' : '?';
|
|
return `${url}${separator}${key}=${encodeURIComponent(value)}`;
|
|
}
|
|
}
|
|
```
|
|
|
|
## 进度监控
|
|
|
|
### 更新进度回调
|
|
```typescript
|
|
export interface HotUpdateProgress {
|
|
/** 当前进度百分比 (0-100) */
|
|
percent: number;
|
|
|
|
/** 已下载字节数 */
|
|
downloadedBytes: number;
|
|
|
|
/** 总字节数 */
|
|
totalBytes: number;
|
|
|
|
/** 当前下载的文件 */
|
|
currentFile?: string;
|
|
}
|
|
|
|
export interface HotUpdateCallbacks {
|
|
/** 进度回调 */
|
|
onProgress?: (progress: HotUpdateProgress) => void;
|
|
|
|
/** 完成回调 */
|
|
onComplete?: (result: IPromiseResult) => void;
|
|
|
|
/** 错误回调 */
|
|
onError?: (error: IPromiseResult) => void;
|
|
}
|
|
|
|
export class HotUpdate {
|
|
public updateWithCallbacks(callbacks: HotUpdateCallbacks): void {
|
|
this._am.setEventCallback((event) => {
|
|
switch (event.getEventCode()) {
|
|
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
|
|
callbacks.onProgress?.({
|
|
percent: event.getPercent(),
|
|
downloadedBytes: event.getDownloadedBytes(),
|
|
totalBytes: event.getTotalBytes()
|
|
});
|
|
break;
|
|
case jsb.EventAssetsManager.UPDATE_FINISHED:
|
|
callbacks.onComplete?.({ code: HotUpdateCode.Succeed, message: "更新完成" });
|
|
break;
|
|
case jsb.EventAssetsManager.UPDATE_FAILED:
|
|
callbacks.onError?.({ code: HotUpdateCode.DownloadFailed, message: "更新失败" });
|
|
break;
|
|
}
|
|
});
|
|
|
|
this._am.update();
|
|
}
|
|
}
|
|
```
|
|
|
|
## 错误处理和重试
|
|
|
|
### 重试机制
|
|
```typescript
|
|
export class HotUpdate {
|
|
private _retryCount: number = 0;
|
|
private readonly _maxRetryCount: number = 3;
|
|
|
|
/**
|
|
* 带重试的热更新
|
|
*/
|
|
public async hotUpdateWithRetry(): Promise<IPromiseResult> {
|
|
for (let i = 0; i < this._maxRetryCount; i++) {
|
|
try {
|
|
const result = await this.hotUpdate();
|
|
|
|
if (result.code === HotUpdateCode.Succeed) {
|
|
return result;
|
|
}
|
|
|
|
// 网络错误可以重试
|
|
if (result.code === HotUpdateCode.NetworkError && i < this._maxRetryCount - 1) {
|
|
warn(`热更新失败,准备重试 (${i + 1}/${this._maxRetryCount})`);
|
|
await this.delay(1000 * (i + 1)); // 递增延迟
|
|
continue;
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
error('热更新异常', error);
|
|
if (i === this._maxRetryCount - 1) {
|
|
return { code: HotUpdateCode.UnknownError, message: `重试${this._maxRetryCount}次后仍然失败` };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private delay(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
}
|
|
```
|
|
|
|
## 热更新最佳实践
|
|
|
|
### 1. 版本管理
|
|
- 使用语义化版本号 (major.minor.patch)
|
|
- 提供版本比较和检查功能
|
|
- 记录版本更新历史
|
|
|
|
### 2. 网络处理
|
|
- 实现重试机制处理网络不稳定
|
|
- 添加超时控制
|
|
- 支持断点续传
|
|
|
|
### 3. 用户体验
|
|
- 提供详细的进度反馈
|
|
- 支持后台下载
|
|
- 提供更新取消选项
|
|
|
|
### 4. 错误恢复
|
|
- 验证下载文件完整性
|
|
- 支持回滚到上一版本
|
|
- 提供修复工具清理损坏文件
|
|
|
|
### 5. 安全性
|
|
- 验证manifest签名
|
|
- 检查文件哈希值
|
|
- 使用HTTPS传输 |