基于 Cocos Creator 的 AssetsManager 热更新机制,本方案实现以下能力:
- ✅ CDN 地址运行时动态下发,无需发包即可切换更新源
- ✅ 主备容灾自动切换,降低单点故障风险
- ✅ 灰度发布,支持按比例逐步放量
- ✅ 本地缓存优化,优先复用上次可用地址
- ✅ 可扩展配置体系,便于后续增加策略与字段
一、整体流程
┌──────────────┐
│ App 启动 │
└──────┬───────┘
↓
┌────────────────────┐
│ 读取本地缓存 URL │
└──────┬─────────────┘
↓
┌────────────────────┐
│ 拉取远程配置 JSON │
└──────┬─────────────┘
↓
┌────────────────────┐
│ URL 决策(灰度/容灾)│
└──────┬─────────────┘
↓
┌────────────────────┐
│ 重写 Manifest │
└──────┬─────────────┘
↓
┌────────────────────┐
│ 初始化 AssetsManager │
└──────┬─────────────┘
↓
┌────────────────────┐
│ checkUpdate │
└────────────────────┘
二、模块拆分
| 模块 | 文件 | 职责 |
|---|---|---|
| ConfigService | config-service.ts | 拉取远程配置 |
| UrlSelector | url-selector.ts | 灰度与容灾决策 |
| ManifestUtil | manifest-util.ts | 动态重写 manifest |
| HotUpdateManager | hot-update.ts | 热更新主流程控制 |
三、代码实现
1. ConfigService(远程配置拉取)
负责请求配置中心,获取 CDN 列表、灰度比例等信息。建议配合 HTTPS、签名校验与缓存策略进一步增强安全性与稳定性。
tsx
export interface RemoteConfig {
version: number;
cdn: string[];
gray?: number;
}
const CONFIG_URL = "https://config.xxx.com/game.json";
const TIMEOUT = 3000;
export class ConfigService {
static async fetch(): Promise<RemoteConfig | null> {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.timeout = TIMEOUT;
xhr.open("GET", CONFIG_URL);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const json = JSON.parse(xhr.responseText);
resolve(json);
} catch {
resolve(null);
}
} else {
resolve(null);
}
}
};
xhr.ontimeout = () => resolve(null);
xhr.send();
});
}
}
2. UrlSelector(灰度 + 容灾)
将"分流"和"地址选择"抽离成独立模块,便于后续替换为更稳定的 hash(如 CRC32 库)或引入更复杂的路由策略。
tsx
export class UrlSelector {
// CRC32 简化实现(可替换为成熟库)
static hash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
// 基于 uid 的稳定分流:同一 uid 结果尽量保持一致
static pick(uid: string, cdns: string[]): string {
const index = this.hash(uid) % cdns.length;
return cdns[index];
}
// 按百分比灰度:返回一个候选列表,供后续 pick 或容灾逻辑使用
static applyGray(cdns: string[], gray: number): string[] {
if (!gray) return cdns;
const rand = Math.random() * 100;
if (rand < gray) {
return [cdns[0]]; // 新 CDN
}
return [cdns[1] || cdns[0]]; // 老 CDN(兜底)
}
}
3. ManifestUtil(动态重写)
通过重写 manifest 中的地址字段,实现热更新源在运行时切换。注意 baseUrl 的尾斜杠处理,避免拼接出错。
tsx
export class ManifestUtil {
static rewrite(content: string, baseUrl: string): string {
const manifest = JSON.parse(content);
const root = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
manifest.packageUrl = root;
manifest.remoteManifestUrl = root + "project.manifest";
manifest.remoteVersionUrl = root + "version.manifest";
return JSON.stringify(manifest);
}
static generateTemp(manifestStr: string): string {
const path = jsb.fileUtils.getWritablePath() + "temp.manifest";
jsb.fileUtils.writeStringToFile(manifestStr, path);
return path;
}
}
4. HotUpdateManager(核心流程)
主流程包含:读取缓存、拉取配置、灰度处理、选择最终 URL、重写 manifest、初始化 AssetsManager、触发更新。
tsx
const DEFAULT_URL = "https://cdn-default.xxx.com/game/";
const STORAGE_PATH = jsb.fileUtils.getWritablePath() + "update/";
export class HotUpdateManager {
private _am: jsb.AssetsManager | null = null;
async start(localManifestPath: string, uid: string) {
// 1. 本地缓存优先
const cacheUrl = cc.sys.localStorage.getItem("HOT_UPDATE_URL");
// 2. 拉取远程配置
const config = await ConfigService.fetch();
let cdns: string[] = [DEFAULT_URL];
if (config && config.cdn && config.cdn.length > 0) {
cdns = config.cdn;
// 灰度处理
cdns = UrlSelector.applyGray(cdns, config.gray || 0);
}
// 3. 选最终 URL(缓存命中则直接用缓存)
const finalUrl = cacheUrl || UrlSelector.pick(uid, cdns);
// 4. 读取本地 manifest
const content = jsb.fileUtils.getStringFromFile(localManifestPath);
// 5. 重写 manifest
const newManifest = ManifestUtil.rewrite(content, finalUrl);
// 6. 生成临时文件
const tempPath = ManifestUtil.generateTemp(newManifest);
// 7. 初始化
this._am = new jsb.AssetsManager(tempPath, STORAGE_PATH);
// 8. 绑定回调
this._am.setEventCallback(this._onUpdateEvent.bind(this));
// 9. 开始检测
this._am.checkUpdate();
}
private _onUpdateEvent(event: jsb.EventAssetsManager) {
switch (event.getEventCode()) {
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
this._am?.update();
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
console.log("更新完成");
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
console.warn("更新失败,尝试重试");
this._am?.downloadFailedAssets();
break;
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
console.error("本地 manifest 缺失");
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
console.error("manifest 下载失败");
break;
}
}
}
四、关键数据结构
远程配置 JSON 示例
json
{
"version": 3,
"cdn": [
"https://cdn-a.xxx.com/game/",
"https://cdn-b.xxx.com/game/"
],
"gray": 20
}
五、容灾策略对比
| 策略 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 单 CDN | 固定地址 | 简单 | 无容灾 |
| 主备切换 | 失败后切换 | 稳定 | 切换慢 |
| 并行探测 | 同时检测多个 CDN | 切换快 | 成本高 |
| 灰度路由 | 按用户分流 | 可控 | 实现复杂 |
六、核心时序
Client ConfigServer CDN
| | |
|---- 请求配置 ------------>| |
|<--- 返回 JSON ------------| |
| | |
|---- 请求 version -------->|------------------->|
|<--- 返回 version ---------|<-------------------|
| | |
|---- 下载资源 ----------->|------------------->|
|<--- 返回资源 ------------|<-------------------|