2536 lines
81 KiB
Markdown
2536 lines
81 KiB
Markdown
# TSGame 应用启动流程详解
|
||
|
||
## 目录
|
||
|
||
1. [概述](#概述)
|
||
2. [启动流程时序](#启动流程时序)
|
||
3. [远程配置文件管理](#远程配置文件管理)
|
||
- [3.1 配置文件结构](#31-配置文件结构)
|
||
- [3.2 配置参数获取逻辑](#32-配置参数获取逻辑)
|
||
- [3.3 配置属性详解](#33-配置属性详解)
|
||
- [3.4 应用升级验证详解](#34-应用升级验证详解)
|
||
- [3.5 下载地址获取详解](#35-下载地址获取详解)
|
||
- [3.6 解压路径和启动路径详解](#36-解压路径和启动路径详解)
|
||
- [3.7 大厅跳转子游戏的具体逻辑详解](#37-大厅跳转子游戏的具体逻辑详解)
|
||
4. [资源目录和解压路径](#资源目录和解压路径)
|
||
- [4.1 文件系统结构](#41-文件系统结构)
|
||
- [4.2 ZIP包解压路径规范](#42-zip包解压路径规范)
|
||
5. [大厅启动逻辑](#大厅启动逻辑)
|
||
- [5.1 大厅资源初始化](#51-大厅资源初始化)
|
||
- [5.2 大厅WebView启动](#52-大厅webview启动)
|
||
6. [子游戏跳转启动逻辑](#子游戏跳转启动逻辑)
|
||
- [6.1 子游戏资源管理](#61-子游戏资源管理)
|
||
- [6.2 子游戏WebView切换](#62-子游戏webview切换)
|
||
- [6.3 子游戏启动流程的配置属性读取详解](#63-子游戏启动流程的配置属性读取详解)
|
||
- [6.4 子游戏启动路径的完整解析](#64-子游戏启动路径的完整解析)
|
||
7. [版本检查和更新流程](#版本检查和更新流程)
|
||
8. [启动异常处理](#启动异常处理)
|
||
|
||
---
|
||
|
||
## 概述
|
||
|
||
TSGame HarmonyOS应用采用分阶段启动流程,通过远程配置文件驱动,支持大厅和子游戏的动态资源管理。应用启动过程包括配置文件获取、资源下载解压、WebView初始化和页面加载等关键步骤。
|
||
|
||
### 核心特性
|
||
|
||
- **远程配置驱动**:通过七牛云CDN获取分层配置
|
||
- **双WebView架构**:大厅和子游戏独立管理
|
||
- **动态资源更新**:支持ZIP包热更新
|
||
- **版本检查机制**:自动检测和更新资源版本
|
||
- **降级兼容**:配置获取失败时使用本地缓存
|
||
|
||
---
|
||
|
||
## 启动流程时序
|
||
|
||
### 完整启动时序图
|
||
|
||
```
|
||
用户启动App
|
||
↓
|
||
EntryAbility.onCreate()
|
||
↓
|
||
初始化基础管理器
|
||
↓
|
||
ConfigManager.loadStartupConfig() ←→ 加载本地启动配置
|
||
↓
|
||
检测远程配置文件 ←→ 七牛云CDN
|
||
↓ ↓
|
||
配置文件存在? 下载配置文件
|
||
↓ (是) ↓
|
||
解析远程配置 校验文件格式
|
||
↓ ↓
|
||
获取大厅ZIP包URL 保存到本地缓存
|
||
↓
|
||
下载大厅ZIP包 ←→ CDN服务器
|
||
↓ ↓
|
||
解压大厅资源 验证ZIP包完整性
|
||
↓
|
||
初始化大厅WebView
|
||
↓
|
||
加载大厅HTML页面
|
||
↓
|
||
大厅显示完成
|
||
↓
|
||
用户点击子游戏
|
||
↓
|
||
获取子游戏ZIP包URL
|
||
↓
|
||
验证子游戏ZIP包有效性
|
||
↓
|
||
下载子游戏ZIP包 ←→ CDN服务器
|
||
↓ ↓
|
||
解压子游戏资源 验证ZIP包完整性
|
||
↓
|
||
初始化子游戏WebView
|
||
↓
|
||
加载子游戏HTML页面
|
||
↓
|
||
子游戏启动完成
|
||
```
|
||
|
||
### 启动阶段详解
|
||
|
||
#### 阶段1:应用初始化
|
||
|
||
```typescript
|
||
class EntryAbility extends UIAbility {
|
||
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
|
||
console.log('TSGame应用启动');
|
||
|
||
// 1. 初始化基础服务
|
||
this.initBaseServices();
|
||
|
||
// 2. 加载启动配置
|
||
this.loadStartupConfig();
|
||
|
||
// 3. 启动配置检查流程
|
||
this.startConfigCheckFlow();
|
||
}
|
||
|
||
private async initBaseServices(): Promise<void> {
|
||
// 初始化文件管理器
|
||
await FileManager.initialize();
|
||
|
||
// 初始化网络管理器
|
||
await NetworkManager.initialize();
|
||
|
||
// 初始化配置管理器
|
||
await ConfigManager.initialize();
|
||
|
||
// 初始化资源管理器
|
||
await ResourceManager.initialize();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 阶段2:配置文件检查
|
||
|
||
```typescript
|
||
class ConfigManager {
|
||
|
||
/**
|
||
* 启动配置检查流程
|
||
*/
|
||
public async startConfigCheckFlow(): Promise<void> {
|
||
try {
|
||
// 1. 加载本地启动配置
|
||
const localConfig = await this.loadLocalStartupConfig();
|
||
|
||
// 2. 获取远程配置文件
|
||
const remoteConfig = await this.fetchRemoteConfig();
|
||
|
||
// 3. 比较并更新配置
|
||
const needUpdate = this.compareConfigs(localConfig, remoteConfig);
|
||
|
||
if (needUpdate) {
|
||
// 4. 更新本地配置缓存
|
||
await this.updateLocalConfig(remoteConfig);
|
||
}
|
||
|
||
// 5. 启动大厅初始化流程
|
||
await this.startHallInitialization(remoteConfig);
|
||
|
||
} catch (error) {
|
||
console.error('配置检查失败:', error);
|
||
// 降级到本地配置
|
||
await this.fallbackToLocalConfig();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取远程配置文件
|
||
*/
|
||
private async fetchRemoteConfig(): Promise<RemoteConfig> {
|
||
const gameData = await this.getCurrentGameData();
|
||
const configUrl = this.buildConfigUrl(gameData);
|
||
|
||
console.log('请求配置文件URL:', configUrl);
|
||
|
||
const response = await fetch(configUrl + '?t=' + Date.now(), {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Cache-Control': 'no-cache'
|
||
},
|
||
timeout: 10000
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`配置文件获取失败: ${response.status}`);
|
||
}
|
||
|
||
const configData = await response.json();
|
||
console.log('远程配置获取成功:', configData);
|
||
|
||
return configData;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 远程配置文件管理
|
||
|
||
### 3.1 配置文件结构
|
||
|
||
#### 配置文件完整JSON结构
|
||
|
||
```json
|
||
{
|
||
"configVersion": "1.0.0",
|
||
"timestamp": 1688544000000,
|
||
"data": {
|
||
"app_download": "https://cdn.jxjygame.com/app/tsgame_v3.6.3.apk",
|
||
"app_version": "3.6.3",
|
||
"app_size": "25.6MB",
|
||
"game_download": "https://cdn.jxjygame.com/games/default_game.zip",
|
||
"game_version": "1.0.0",
|
||
"game_size": "12.3MB",
|
||
"url": "https://api.jxjygame.com",
|
||
"showmessage": "欢迎使用进贤聚友棋牌",
|
||
"agentlist": [
|
||
{
|
||
"agentid": "agent001",
|
||
"agentname": "代理商A",
|
||
"url": "https://api-agent001.jxjygame.com",
|
||
"app_download": "https://cdn.jxjygame.com/app/agent001_v3.6.3.apk",
|
||
"app_version": "3.6.3",
|
||
"app_size": "25.8MB",
|
||
"game_download": "https://cdn.jxjygame.com/games/agent001_games.zip",
|
||
"game_version": "2.1.0",
|
||
"game_size": "15.2MB",
|
||
"showmessage": "代理商A定制版本",
|
||
"channellist": [
|
||
{
|
||
"channelid": "channel001",
|
||
"channelname": "华为渠道",
|
||
"app_download": "https://cdn.jxjygame.com/app/huawei_v3.6.3.apk",
|
||
"app_version": "3.6.3",
|
||
"app_size": "25.9MB",
|
||
"game_download": "https://cdn.jxjygame.com/games/huawei_games.zip",
|
||
"game_version": "2.1.1",
|
||
"game_size": "15.5MB",
|
||
"showmessage": "华为渠道专版",
|
||
"marketlist": [
|
||
{
|
||
"marketid": "huawei_appgallery",
|
||
"marketname": "华为应用市场",
|
||
"app_download": "https://cdn.jxjygame.com/app/huawei_market_v3.6.4.apk",
|
||
"app_version": "3.6.4",
|
||
"app_size": "26.1MB",
|
||
"game_download": "https://cdn.jxjygame.com/games/huawei_market_games.zip",
|
||
"game_version": "2.1.2",
|
||
"game_size": "15.8MB",
|
||
"showmessage": "华为应用市场版本"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 配置层级说明
|
||
|
||
配置文件采用四级层次结构,支持参数覆盖:
|
||
|
||
1. **全局配置** (data根级别) - 最低优先级
|
||
2. **代理商配置** (agentlist) - 覆盖全局配置
|
||
3. **渠道配置** (channellist) - 覆盖代理商配置
|
||
4. **市场配置** (marketlist) - 最高优先级,覆盖所有上级配置
|
||
|
||
### 3.2 配置参数获取逻辑
|
||
|
||
#### 核心参数获取类
|
||
|
||
```typescript
|
||
/**
|
||
* 配置参数获取工具类
|
||
* 严格按照Android版本的逻辑实现,支持分层参数覆盖
|
||
*/
|
||
export class ConfigParameterHelper {
|
||
|
||
/**
|
||
* 根据层级结构获取配置参数值
|
||
* @param config 配置对象
|
||
* @param paramName 参数名称
|
||
* @param gameData 游戏数据上下文
|
||
* @returns 参数值
|
||
*/
|
||
public static getParameterValue(config: any, paramName: string, gameData: GameData): any {
|
||
const agentId = gameData.agentId;
|
||
const channelId = gameData.channelId;
|
||
const marketId = gameData.marketId;
|
||
|
||
let paramValue = null;
|
||
|
||
// 内部函数:遍历数组匹配key获取参数值
|
||
const getParameterFromList = (arrayList: any[], keyName: string, keyValue: string): any => {
|
||
if (arrayList && keyName && keyValue) {
|
||
for (let i = 0; i < arrayList.length; i++) {
|
||
if (arrayList[i][keyName] === keyValue) {
|
||
// 如果找到匹配项且包含目标参数,更新参数值
|
||
if (arrayList[i][paramName] !== undefined) {
|
||
paramValue = arrayList[i][paramName];
|
||
}
|
||
return arrayList[i];
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// 检查配置数据是否存在
|
||
if (!config || !config.data) {
|
||
return paramValue;
|
||
}
|
||
|
||
// 1. 首先检查全局配置(最低优先级)
|
||
if (config.data[paramName] !== undefined) {
|
||
paramValue = config.data[paramName];
|
||
}
|
||
|
||
// 2. 按层级查找,后面的层级覆盖前面的:代理商 -> 渠道 -> 市场
|
||
|
||
// 代理商级别配置
|
||
const agentConfig = getParameterFromList(config.data.agentlist, "agentid", agentId);
|
||
if (!agentConfig) {
|
||
return paramValue; // 如果找不到代理商配置,返回全局配置值
|
||
}
|
||
|
||
// 渠道级别配置
|
||
const channelConfig = getParameterFromList(agentConfig.channellist, "channelid", channelId);
|
||
if (!channelConfig) {
|
||
return paramValue; // 如果找不到渠道配置,返回当前参数值
|
||
}
|
||
|
||
// 市场级别配置(最高优先级)
|
||
const marketConfig = getParameterFromList(channelConfig.marketlist, "marketid", marketId);
|
||
// 不管是否找到市场配置,都返回当前参数值(可能已被前面层级更新)
|
||
|
||
return paramValue;
|
||
}
|
||
|
||
/**
|
||
* 获取应用下载地址
|
||
* 对应配置文件中的 app_download 属性
|
||
*/
|
||
public static getAppDownloadUrl(config: any, gameData: GameData): string {
|
||
return this.getParameterValue(config, "app_download", gameData) || "";
|
||
}
|
||
|
||
/**
|
||
* 获取应用版本号
|
||
* 对应配置文件中的 app_version 属性
|
||
*/
|
||
public static getAppVersion(config: any, gameData: GameData): string {
|
||
return this.getParameterValue(config, "app_version", gameData) || "";
|
||
}
|
||
|
||
/**
|
||
* 获取游戏ZIP包下载地址
|
||
* 对应配置文件中的 game_download 属性
|
||
*/
|
||
public static getGameDownloadUrl(config: any, gameData: GameData): string {
|
||
return this.getParameterValue(config, "game_download", gameData) || "";
|
||
}
|
||
|
||
/**
|
||
* 获取游戏版本号
|
||
* 对应配置文件中的 game_version 属性
|
||
*/
|
||
public static getGameVersion(config: any, gameData: GameData): string {
|
||
return this.getParameterValue(config, "game_version", gameData) || "";
|
||
}
|
||
|
||
/**
|
||
* 获取服务器地址
|
||
* 对应配置文件中的 url 属性
|
||
*/
|
||
public static getServerUrl(config: any, gameData: GameData): string {
|
||
return this.getParameterValue(config, "url", gameData) || "";
|
||
}
|
||
|
||
/**
|
||
* 获取显示消息
|
||
* 对应配置文件中的 showmessage 属性
|
||
*/
|
||
public static getShowMessage(config: any, gameData: GameData): string {
|
||
return this.getParameterValue(config, "showmessage", gameData) || "";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 游戏数据上下文
|
||
*/
|
||
export interface GameData {
|
||
agentId: string; // 代理商ID,从本地配置或启动参数获取
|
||
channelId: string; // 渠道ID,从本地配置或启动参数获取
|
||
marketId: string; // 市场ID,从本地配置或启动参数获取
|
||
}
|
||
```
|
||
|
||
#### 配置参数获取示例
|
||
|
||
```typescript
|
||
// 配置参数获取的实际使用示例
|
||
export class ConfigUsageExample {
|
||
|
||
public static async demonstrateConfigUsage(): Promise<void> {
|
||
// 1. 获取当前游戏数据上下文
|
||
const gameData: GameData = {
|
||
agentId: "agent001", // 从本地存储或启动参数获取
|
||
channelId: "channel001", // 从本地存储或启动参数获取
|
||
marketId: "huawei_appgallery" // 从本地存储或启动参数获取
|
||
};
|
||
|
||
// 2. 加载远程配置
|
||
const config = await ConfigManager.fetchRemoteConfig();
|
||
|
||
// 3. 获取各种配置参数
|
||
const appDownloadUrl = ConfigParameterHelper.getAppDownloadUrl(config, gameData);
|
||
// 实际获取到: "https://cdn.jxjygame.com/app/huawei_market_v3.6.4.apk"
|
||
|
||
const appVersion = ConfigParameterHelper.getAppVersion(config, gameData);
|
||
// 实际获取到: "3.6.4" (市场级配置覆盖)
|
||
|
||
const gameDownloadUrl = ConfigParameterHelper.getGameDownloadUrl(config, gameData);
|
||
// 实际获取到: "https://cdn.jxjygame.com/games/huawei_market_games.zip"
|
||
|
||
const gameVersion = ConfigParameterHelper.getGameVersion(config, gameData);
|
||
// 实际获取到: "2.1.2" (市场级配置覆盖)
|
||
|
||
const serverUrl = ConfigParameterHelper.getServerUrl(config, gameData);
|
||
// 实际获取到: "https://api-agent001.jxjygame.com" (代理商级配置)
|
||
|
||
const showMessage = ConfigParameterHelper.getShowMessage(config, gameData);
|
||
// 实际获取到: "华为应用市场版本" (市场级配置覆盖)
|
||
|
||
console.log('配置参数获取结果:');
|
||
console.log(`App Download URL: ${appDownloadUrl}`);
|
||
console.log(`App Version: ${appVersion}`);
|
||
console.log(`Game Download URL: ${gameDownloadUrl}`);
|
||
console.log(`Game Version: ${gameVersion}`);
|
||
console.log(`Server URL: ${serverUrl}`);
|
||
console.log(`Show Message: ${showMessage}`);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 配置获取的优先级规则
|
||
|
||
1. **全局配置** (`data`根级别):作为默认值,优先级最低
|
||
2. **代理商配置** (`agentlist[].`):覆盖全局配置中的同名属性
|
||
3. **渠道配置** (`channellist[].`):覆盖代理商和全局配置中的同名属性
|
||
4. **市场配置** (`marketlist[].`):最高优先级,覆盖所有上级配置中的同名属性
|
||
|
||
### 3.4 应用升级验证详解
|
||
|
||
#### 升级验证的具体配置属性
|
||
|
||
TSGame应用的升级验证基于以下配置属性的组合判断:
|
||
|
||
```typescript
|
||
interface AppUpgradeValidation {
|
||
// 主要验证属性
|
||
app_version: string; // 远程应用版本号 - 核心验证属性
|
||
app_download: string; // 应用下载URL - 升级时获取新版本
|
||
app_size: string; // 应用包大小 - 用于下载进度显示
|
||
showmessage: string; // 版本更新说明 - 用户升级提示
|
||
|
||
// 辅助验证属性
|
||
force_update?: boolean; // 强制更新标志 - 是否必须升级
|
||
min_version?: string; // 最低支持版本 - 低于此版本强制升级
|
||
update_priority?: number; // 更新优先级 - 控制更新推送频率
|
||
}
|
||
```
|
||
|
||
#### 具体的升级验证逻辑
|
||
|
||
```typescript
|
||
export class AppUpgradeValidator {
|
||
|
||
/**
|
||
* 验证应用是否需要升级
|
||
* 主要读取配置属性:app_version、force_update、min_version
|
||
*/
|
||
public static async validateAppUpgrade(config: any, gameData: any): Promise<UpgradeInfo> {
|
||
// 1. 获取远程版本号 - 读取 app_version 属性
|
||
const remoteVersion = ConfigParameterHelper.getAppVersion(config, gameData);
|
||
console.log(`远程应用版本: ${remoteVersion}`);
|
||
|
||
// 2. 获取本地应用版本
|
||
const localVersion = await DeviceInfo.getAppVersion();
|
||
console.log(`本地应用版本: ${localVersion}`);
|
||
|
||
// 3. 版本比较 - 核心验证逻辑
|
||
const needUpdate = VersionComparator.isVersionNewer(remoteVersion, localVersion);
|
||
|
||
// 4. 检查是否强制更新 - 读取 force_update 属性
|
||
const forceUpdate = ConfigParameterHelper.getBooleanValue(config, gameData, 'force_update', false);
|
||
|
||
// 5. 检查最低版本要求 - 读取 min_version 属性
|
||
const minVersion = ConfigParameterHelper.getString(config, gameData, 'min_version', '1.0.0');
|
||
const belowMinVersion = VersionComparator.isVersionOlder(localVersion, minVersion);
|
||
|
||
return {
|
||
needUpdate: needUpdate || belowMinVersion,
|
||
forceUpdate: forceUpdate || belowMinVersion,
|
||
remoteVersion,
|
||
localVersion,
|
||
upgradeType: belowMinVersion ? 'critical' : (forceUpdate ? 'force' : 'optional')
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取应用下载信息
|
||
* 主要读取配置属性:app_download、app_size、showmessage
|
||
*/
|
||
public static getAppDownloadInfo(config: any, gameData: any): AppDownloadInfo {
|
||
return {
|
||
// 读取 app_download 属性 - 应用下载地址
|
||
downloadUrl: ConfigParameterHelper.getAppDownloadUrl(config, gameData),
|
||
|
||
// 读取 app_size 属性 - 应用包大小
|
||
packageSize: ConfigParameterHelper.getAppSize(config, gameData),
|
||
|
||
// 读取 showmessage 属性 - 更新说明
|
||
upgradeMessage: ConfigParameterHelper.getShowMessage(config, gameData),
|
||
|
||
// 其他下载相关属性
|
||
md5Hash: ConfigParameterHelper.getString(config, gameData, 'app_md5', ''),
|
||
timeout: ConfigParameterHelper.getNumber(config, gameData, 'download_timeout', 300000)
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.5 下载地址获取详解
|
||
|
||
#### 应用下载地址的具体获取
|
||
|
||
```typescript
|
||
export class DownloadUrlResolver {
|
||
|
||
/**
|
||
* 获取应用下载地址
|
||
* 配置属性:app_download
|
||
*/
|
||
public static getAppDownloadUrl(config: any, gameData: any): string {
|
||
// 按优先级读取 app_download 属性
|
||
const downloadUrl = ConfigParameterHelper.getAppDownloadUrl(config, gameData);
|
||
|
||
console.log(`应用下载地址配置路径解析:`);
|
||
console.log(`1. 尝试读取市场级别: marketlist[${gameData.marketid}].app_download`);
|
||
console.log(`2. 尝试读取渠道级别: channellist[${gameData.channelid}].app_download`);
|
||
console.log(`3. 尝试读取代理商级别: agentlist[${gameData.agentid}].app_download`);
|
||
console.log(`4. 尝试读取全局级别: data.app_download`);
|
||
console.log(`最终获取到的下载地址: ${downloadUrl}`);
|
||
|
||
return downloadUrl;
|
||
}
|
||
|
||
/**
|
||
* 获取大厅ZIP包下载地址
|
||
* 配置属性:game_download(大厅使用相同的配置)
|
||
*/
|
||
public static getHallZipDownloadUrl(config: any, gameData: any): string {
|
||
// 大厅ZIP包使用 game_download 属性
|
||
const hallZipUrl = ConfigParameterHelper.getGameDownloadUrl(config, gameData);
|
||
|
||
console.log(`大厅ZIP包下载地址: ${hallZipUrl}`);
|
||
return hallZipUrl;
|
||
}
|
||
|
||
/**
|
||
* 获取子游戏ZIP包下载地址
|
||
* 配置属性:game_download + 游戏ID拼接
|
||
*/
|
||
public static getGameZipDownloadUrl(config: any, gameData: any, gameId: string): string {
|
||
// 基础下载地址
|
||
const baseDownloadUrl = ConfigParameterHelper.getGameDownloadUrl(config, gameData);
|
||
|
||
// 拼接游戏ID构成完整下载地址
|
||
const gameZipUrl = `${baseDownloadUrl}/${gameId}.zip`;
|
||
|
||
console.log(`子游戏${gameId}的ZIP包下载地址: ${gameZipUrl}`);
|
||
return gameZipUrl;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.6 解压路径和启动路径详解
|
||
|
||
#### 大厅ZIP解压路径详解
|
||
|
||
```typescript
|
||
export class HallPathResolver {
|
||
|
||
/**
|
||
* 大厅ZIP包解压的完整路径规范
|
||
*/
|
||
public static getHallPaths(): HallPathInfo {
|
||
const appRootPath = getContext().filesDir;
|
||
|
||
return {
|
||
// 1. 大厅ZIP包下载临时路径
|
||
downloadTempPath: `${appRootPath}/downloads/hall_temp.zip`,
|
||
|
||
// 2. 大厅资源解压目标路径
|
||
extractTargetPath: `${appRootPath}/game_resources/hall`,
|
||
|
||
// 3. 大厅启动入口文件路径
|
||
entryFilePath: `${appRootPath}/game_resources/hall/index.html`,
|
||
|
||
// 4. 大厅资源验证路径
|
||
versionFilePath: `${appRootPath}/game_resources/hall/version.txt`,
|
||
configFilePath: `${appRootPath}/game_resources/hall/config.json`,
|
||
|
||
// 5. 大厅WebView加载URL
|
||
webViewLoadUrl: `file://${appRootPath}/game_resources/hall/index.html`
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 大厅解压流程详解
|
||
*/
|
||
public static async extractHallZip(zipFilePath: string): Promise<void> {
|
||
const paths = this.getHallPaths();
|
||
|
||
console.log(`=== 大厅ZIP包解压流程 ===`);
|
||
console.log(`源文件: ${zipFilePath}`);
|
||
console.log(`目标路径: ${paths.extractTargetPath}`);
|
||
|
||
// 1. 清理旧的大厅资源
|
||
if (await fileIo.access(paths.extractTargetPath)) {
|
||
await fileIo.rmdir(paths.extractTargetPath, true);
|
||
console.log(`已清理旧大厅资源: ${paths.extractTargetPath}`);
|
||
}
|
||
|
||
// 2. 创建解压目标目录
|
||
await fileIo.mkdir(paths.extractTargetPath, true);
|
||
|
||
// 3. 执行ZIP解压
|
||
await ZipExtractor.extractToDirectory(zipFilePath, paths.extractTargetPath);
|
||
|
||
// 4. 验证关键文件
|
||
await this.validateHallFiles(paths);
|
||
|
||
console.log(`大厅ZIP包解压完成,启动路径: ${paths.webViewLoadUrl}`);
|
||
}
|
||
|
||
/**
|
||
* 验证大厅文件完整性
|
||
*/
|
||
private static async validateHallFiles(paths: HallPathInfo): Promise<void> {
|
||
const requiredFiles = [
|
||
paths.entryFilePath, // index.html - 必须存在
|
||
`${paths.extractTargetPath}/js/main.js`, // 主逻辑文件
|
||
`${paths.extractTargetPath}/css/main.css` // 主样式文件
|
||
];
|
||
|
||
for (const filePath of requiredFiles) {
|
||
if (!await fileIo.access(filePath)) {
|
||
throw new Error(`大厅关键文件缺失: ${filePath}`);
|
||
}
|
||
}
|
||
|
||
console.log(`大厅文件验证通过`);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 子游戏ZIP解压路径详解
|
||
|
||
```typescript
|
||
export class GamePathResolver {
|
||
|
||
/**
|
||
* 子游戏ZIP包解压的完整路径规范
|
||
*/
|
||
public static getGamePaths(gameId: string): GamePathInfo {
|
||
const appRootPath = getContext().filesDir;
|
||
|
||
return {
|
||
// 1. 子游戏ZIP包下载临时路径
|
||
downloadTempPath: `${appRootPath}/downloads/${gameId}_temp.zip`,
|
||
|
||
// 2. 子游戏资源解压目标路径
|
||
extractTargetPath: `${appRootPath}/game_resources/games/${gameId}`,
|
||
|
||
// 3. 子游戏启动入口文件路径
|
||
entryFilePath: `${appRootPath}/game_resources/games/${gameId}/index.html`,
|
||
|
||
// 4. 子游戏资源验证路径
|
||
versionFilePath: `${appRootPath}/game_resources/games/${gameId}/version.txt`,
|
||
configFilePath: `${appRootPath}/game_resources/games/${gameId}/game.json`,
|
||
|
||
// 5. 子游戏WebView加载URL
|
||
webViewLoadUrl: `file://${appRootPath}/game_resources/games/${gameId}/index.html`,
|
||
|
||
// 6. 子游戏资源子目录
|
||
jsDir: `${appRootPath}/game_resources/games/${gameId}/js`,
|
||
cssDir: `${appRootPath}/game_resources/games/${gameId}/css`,
|
||
imagesDir: `${appRootPath}/game_resources/games/${gameId}/images`,
|
||
audioDir: `${appRootPath}/game_resources/games/${gameId}/audio`
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 子游戏解压流程详解
|
||
*/
|
||
public static async extractGameZip(zipFilePath: string, gameId: string): Promise<void> {
|
||
const paths = this.getGamePaths(gameId);
|
||
|
||
console.log(`=== 子游戏${gameId}的ZIP包解压流程 ===`);
|
||
console.log(`源文件: ${zipFilePath}`);
|
||
console.log(`目标路径: ${paths.extractTargetPath}`);
|
||
|
||
// 1. 清理旧的游戏资源
|
||
if (await fileIo.access(paths.extractTargetPath)) {
|
||
await fileIo.rmdir(paths.extractTargetPath, true);
|
||
console.log(`已清理旧游戏${gameId}资源: ${paths.extractTargetPath}`);
|
||
}
|
||
|
||
// 2. 创建游戏目录结构
|
||
await this.createGameDirectories(paths);
|
||
|
||
// 3. 执行ZIP解压
|
||
await ZipExtractor.extractToDirectory(zipFilePath, paths.extractTargetPath);
|
||
|
||
// 4. 验证游戏文件
|
||
await this.validateGameFiles(gameId, paths);
|
||
|
||
console.log(`子游戏${gameId}解压完成,启动路径: ${paths.webViewLoadUrl}`);
|
||
}
|
||
|
||
/**
|
||
* 创建游戏目录结构
|
||
*/
|
||
private static async createGameDirectories(paths: GamePathInfo): Promise<void> {
|
||
const directories = [
|
||
paths.extractTargetPath,
|
||
paths.jsDir,
|
||
paths.cssDir,
|
||
paths.imagesDir,
|
||
paths.audioDir
|
||
];
|
||
|
||
for (const dir of directories) {
|
||
await fileIo.mkdir(dir, true);
|
||
}
|
||
|
||
console.log(`游戏目录结构创建完成`);
|
||
}
|
||
|
||
/**
|
||
* 验证游戏文件完整性
|
||
*/
|
||
private static async validateGameFiles(gameId: string, paths: GamePathInfo): Promise<void> {
|
||
const requiredFiles = [
|
||
paths.entryFilePath, // index.html - 游戏入口
|
||
`${paths.jsDir}/game.js`, // 游戏主逻辑
|
||
`${paths.cssDir}/game.css` // 游戏样式
|
||
];
|
||
|
||
for (const filePath of requiredFiles) {
|
||
if (!await fileIo.access(filePath)) {
|
||
throw new Error(`游戏${gameId}关键文件缺失: ${filePath}`);
|
||
}
|
||
}
|
||
|
||
console.log(`游戏${gameId}文件验证通过`);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.7 大厅跳转子游戏的具体逻辑详解
|
||
|
||
#### 跳转触发和参数传递
|
||
|
||
```typescript
|
||
export class HallGameSwitchController {
|
||
|
||
/**
|
||
* 大厅跳转子游戏的完整流程
|
||
* 这是从大厅点击游戏到子游戏启动的详细逻辑
|
||
*/
|
||
public static async switchFromHallToGame(gameId: string, gameData: any): Promise<void> {
|
||
try {
|
||
console.log(`=== 大厅跳转子游戏流程开始 ===`);
|
||
console.log(`目标游戏ID: ${gameId}`);
|
||
console.log(`游戏数据:`, gameData);
|
||
|
||
// 第一步:在大厅WebView中接收跳转请求
|
||
await this.handleHallGameClick(gameId, gameData);
|
||
|
||
// 第二步:检查子游戏资源状态
|
||
const resourceStatus = await this.checkGameResourceStatus(gameId);
|
||
|
||
// 第三步:根据资源状态决定后续流程
|
||
if (resourceStatus.needDownload) {
|
||
await this.downloadAndPrepareGame(gameId, resourceStatus);
|
||
}
|
||
|
||
// 第四步:切换WebView并启动子游戏
|
||
await this.switchWebViewToGame(gameId, gameData);
|
||
|
||
console.log(`=== 大厅跳转子游戏流程完成 ===`);
|
||
|
||
} catch (error) {
|
||
console.error(`大厅跳转子游戏失败:`, error);
|
||
await this.handleSwitchError(gameId, error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 第一步:处理大厅中的游戏点击事件
|
||
*/
|
||
private static async handleHallGameClick(gameId: string, gameData: any): Promise<void> {
|
||
console.log(`=== 第一步:处理大厅游戏点击 ===`);
|
||
|
||
// 1. 在大厅WebView中触发JSBridge调用
|
||
// 大厅JS代码会调用: window.WebViewJavascriptBridge.callHandler('SwitchOverGameData', {...})
|
||
|
||
// 2. 验证游戏参数
|
||
this.validateGameSwitchParams(gameId, gameData);
|
||
|
||
// 3. 记录跳转日志用于统计
|
||
await Logger.logGameSwitch(gameId, 'hall_click', gameData);
|
||
|
||
// 4. 显示切换加载界面
|
||
await UIController.showGameSwitchLoading(gameId);
|
||
|
||
console.log(`大厅游戏点击处理完成`);
|
||
}
|
||
|
||
/**
|
||
* 第二步:检查子游戏资源状态
|
||
*/
|
||
private static async checkGameResourceStatus(gameId: string): Promise<GameResourceStatus> {
|
||
console.log(`=== 第二步:检查游戏${gameId}资源状态 ===`);
|
||
|
||
const paths = GamePathResolver.getGamePaths(gameId);
|
||
const config = await ConfigManager.getRemoteConfig();
|
||
const gameData = await ConfigManager.getCurrentGameData();
|
||
|
||
// 1. 检查游戏目录是否存在
|
||
const gameExists = await fileIo.access(paths.extractTargetPath);
|
||
console.log(`游戏目录存在: ${gameExists}`);
|
||
|
||
if (!gameExists) {
|
||
return {
|
||
needDownload: true,
|
||
reason: 'game_not_exists',
|
||
downloadUrl: DownloadUrlResolver.getGameZipDownloadUrl(config, gameData, gameId)
|
||
};
|
||
}
|
||
|
||
// 2. 检查游戏版本是否需要更新
|
||
const remoteVersion = ConfigParameterHelper.getGameVersion(config, gameData);
|
||
const localVersion = await this.getLocalGameVersion(gameId, paths);
|
||
|
||
console.log(`远程游戏版本: ${remoteVersion}`);
|
||
console.log(`本地游戏版本: ${localVersion}`);
|
||
|
||
const needUpdate = VersionComparator.isVersionNewer(remoteVersion, localVersion);
|
||
|
||
if (needUpdate) {
|
||
return {
|
||
needDownload: true,
|
||
reason: 'version_update',
|
||
downloadUrl: DownloadUrlResolver.getGameZipDownloadUrl(config, gameData, gameId),
|
||
oldVersion: localVersion,
|
||
newVersion: remoteVersion
|
||
};
|
||
}
|
||
|
||
// 3. 验证游戏文件完整性
|
||
const isValid = await this.validateGameIntegrity(gameId, paths);
|
||
|
||
if (!isValid) {
|
||
return {
|
||
needDownload: true,
|
||
reason: 'file_corruption',
|
||
downloadUrl: DownloadUrlResolver.getGameZipDownloadUrl(config, gameData, gameId)
|
||
};
|
||
}
|
||
|
||
console.log(`游戏${gameId}资源检查通过,无需下载`);
|
||
return {
|
||
needDownload: false,
|
||
reason: 'ready',
|
||
launchUrl: paths.webViewLoadUrl
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 第三步:下载并准备游戏资源(如果需要)
|
||
*/
|
||
private static async downloadAndPrepareGame(gameId: string, resourceStatus: GameResourceStatus): Promise<void> {
|
||
console.log(`=== 第三步:下载并准备游戏${gameId}资源 ===`);
|
||
console.log(`下载原因: ${resourceStatus.reason}`);
|
||
console.log(`下载地址: ${resourceStatus.downloadUrl}`);
|
||
|
||
const paths = GamePathResolver.getGamePaths(gameId);
|
||
|
||
// 1. 显示下载进度界面
|
||
await UIController.showGameDownloadProgress(gameId, resourceStatus);
|
||
|
||
// 2. 下载游戏ZIP包
|
||
await FileDownloader.downloadWithProgress(
|
||
resourceStatus.downloadUrl,
|
||
paths.downloadTempPath,
|
||
{
|
||
onProgress: (progress: number, total: number) => {
|
||
const percent = Math.floor((progress / total) * 100);
|
||
UIController.updateGameDownloadProgress(gameId, percent);
|
||
},
|
||
timeout: 300000 // 5分钟超时
|
||
}
|
||
);
|
||
|
||
console.log(`游戏${gameId}ZIP包下载完成: ${paths.downloadTempPath}`);
|
||
|
||
// 3. 解压游戏资源
|
||
await GamePathResolver.extractGameZip(paths.downloadTempPath, gameId);
|
||
|
||
// 4. 清理临时文件
|
||
await fileIo.unlink(paths.downloadTempPath);
|
||
|
||
// 5. 隐藏下载进度界面
|
||
await UIController.hideGameDownloadProgress(gameId);
|
||
|
||
console.log(`游戏${gameId}资源准备完成`);
|
||
}
|
||
|
||
/**
|
||
* 第四步:切换WebView并启动子游戏
|
||
*/
|
||
private static async switchWebViewToGame(gameId: string, gameData: any): Promise<void> {
|
||
console.log(`=== 第四步:切换WebView启动游戏${gameId} ===`);
|
||
|
||
const paths = GamePathResolver.getGamePaths(gameId);
|
||
|
||
// 1. 隐藏大厅WebView
|
||
await WebViewManager.hideHallWebView();
|
||
console.log(`大厅WebView已隐藏`);
|
||
|
||
// 2. 初始化子游戏WebView
|
||
await WebViewManager.initGameWebView(gameId);
|
||
console.log(`子游戏WebView初始化完成`);
|
||
|
||
// 3. 配置子游戏WebView参数
|
||
await this.configureGameWebView(gameId, gameData);
|
||
|
||
// 4. 加载子游戏页面
|
||
console.log(`开始加载游戏页面: ${paths.webViewLoadUrl}`);
|
||
await WebViewManager.loadGamePage(gameId, paths.webViewLoadUrl);
|
||
|
||
// 5. 显示子游戏WebView
|
||
await WebViewManager.showGameWebView(gameId);
|
||
console.log(`子游戏WebView已显示`);
|
||
|
||
// 6. 向子游戏传递启动参数
|
||
await this.passGameStartupParams(gameId, gameData);
|
||
|
||
// 7. 隐藏加载界面
|
||
await UIController.hideGameSwitchLoading();
|
||
|
||
console.log(`游戏${gameId}启动完成`);
|
||
}
|
||
|
||
/**
|
||
* 配置子游戏WebView的特定参数
|
||
*/
|
||
private static async configureGameWebView(gameId: string, gameData: any): Promise<void> {
|
||
const webView = WebViewManager.getGameWebView(gameId);
|
||
|
||
// 1. 设置用户代理
|
||
webView.setUserAgent(`TSGame-HarmonyOS/${await DeviceInfo.getAppVersion()} Game/${gameId}`);
|
||
|
||
// 2. 设置JavaScript Bridge
|
||
await JSBridgeManager.setupGameBridge(gameId, webView);
|
||
|
||
// 3. 注册游戏专用接口
|
||
await this.registerGameSpecificHandlers(gameId, webView);
|
||
|
||
console.log(`游戏${gameId}的WebView配置完成`);
|
||
}
|
||
|
||
/**
|
||
* 向子游戏传递启动参数
|
||
*/
|
||
private static async passGameStartupParams(gameId: string, gameData: any): Promise<void> {
|
||
const webView = WebViewManager.getGameWebView(gameId);
|
||
|
||
// 构造游戏启动参数
|
||
const startupParams = {
|
||
gameId: gameId,
|
||
userId: gameData.userId,
|
||
userToken: gameData.userToken,
|
||
serverUrl: ConfigParameterHelper.getServerUrl(await ConfigManager.getRemoteConfig(), gameData),
|
||
fromPage: 'hall',
|
||
timestamp: Date.now(),
|
||
deviceInfo: await DeviceInfo.getBasicInfo()
|
||
};
|
||
|
||
// 通过JSBridge传递参数给游戏
|
||
webView.callHandler('onGameInit', startupParams, (response) => {
|
||
console.log(`游戏${gameId}初始化回调:`, response);
|
||
});
|
||
|
||
console.log(`游戏${gameId}启动参数传递完成:`, startupParams);
|
||
}
|
||
|
||
/**
|
||
* 获取本地游戏版本
|
||
*/
|
||
private static async getLocalGameVersion(gameId: string, paths: GamePathInfo): Promise<string> {
|
||
try {
|
||
if (await fileIo.access(paths.versionFilePath)) {
|
||
const versionContent = await fileIo.readText(paths.versionFilePath);
|
||
return versionContent.trim();
|
||
}
|
||
} catch (error) {
|
||
console.warn(`读取游戏${gameId}版本失败:`, error);
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 验证游戏文件完整性
|
||
*/
|
||
private static async validateGameIntegrity(gameId: string, paths: GamePathInfo): Promise<boolean> {
|
||
try {
|
||
// 检查关键文件是否存在
|
||
const requiredFiles = [
|
||
paths.entryFilePath,
|
||
`${paths.jsDir}/game.js`,
|
||
`${paths.cssDir}/game.css`
|
||
];
|
||
|
||
for (const filePath of requiredFiles) {
|
||
if (!await fileIo.access(filePath)) {
|
||
console.warn(`游戏${gameId}关键文件缺失: ${filePath}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`验证游戏${gameId}完整性失败:`, error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理切换错误
|
||
*/
|
||
private static async handleSwitchError(gameId: string, error: Error): Promise<void> {
|
||
console.error(`游戏${gameId}切换失败:`, error);
|
||
|
||
// 隐藏加载界面
|
||
await UIController.hideGameSwitchLoading();
|
||
|
||
// 显示错误提示
|
||
await UIController.showErrorDialog(`游戏启动失败`, error.message);
|
||
|
||
// 记录错误日志
|
||
await Logger.logError('GAME_SWITCH_ERROR', {
|
||
gameId,
|
||
error: error.message,
|
||
stack: error.stack
|
||
});
|
||
}
|
||
}
|
||
|
||
// 相关数据结构定义
|
||
interface GameResourceStatus {
|
||
needDownload: boolean;
|
||
reason: 'game_not_exists' | 'version_update' | 'file_corruption' | 'ready';
|
||
downloadUrl?: string;
|
||
oldVersion?: string;
|
||
newVersion?: string;
|
||
launchUrl?: string;
|
||
}
|
||
|
||
interface HallPathInfo {
|
||
downloadTempPath: string;
|
||
extractTargetPath: string;
|
||
entryFilePath: string;
|
||
versionFilePath: string;
|
||
configFilePath: string;
|
||
webViewLoadUrl: string;
|
||
}
|
||
|
||
interface GamePathInfo {
|
||
downloadTempPath: string;
|
||
extractTargetPath: string;
|
||
entryFilePath: string;
|
||
versionFilePath: string;
|
||
configFilePath: string;
|
||
webViewLoadUrl: string;
|
||
jsDir: string;
|
||
cssDir: string;
|
||
imagesDir: string;
|
||
audioDir: string;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 资源目录和解压路径
|
||
|
||
### 4.1 文件系统结构
|
||
|
||
#### HarmonyOS应用文件系统布局
|
||
|
||
```
|
||
/data/app/el2/100/base/com.jx.jyhd.harmonyos/haps/entry/files/
|
||
├── game_resources/ # 游戏资源根目录
|
||
│ ├── hall/ # 大厅资源目录
|
||
│ │ ├── index.html # 大厅入口文件
|
||
│ │ ├── js/ # 大厅JS文件
|
||
│ │ │ ├── main.js # 大厅主要逻辑
|
||
│ │ │ ├── bridge.js # JSBridge桥接文件
|
||
│ │ │ └── utils.js # 工具函数
|
||
│ │ ├── css/ # 大厅样式文件
|
||
│ │ │ ├── main.css # 主样式
|
||
│ │ │ └── responsive.css # 响应式样式
|
||
│ │ ├── images/ # 大厅图片资源
|
||
│ │ │ ├── logo.png # 应用Logo
|
||
│ │ │ ├── bg.jpg # 背景图片
|
||
│ │ │ └── icons/ # 图标文件夹
|
||
│ │ └── version.js # 大厅版本信息
|
||
│ ├── games/ # 子游戏资源目录
|
||
│ │ ├── game001/ # 斗地主游戏
|
||
│ │ │ ├── index.html # 游戏入口文件
|
||
│ │ │ ├── js/ # 游戏JS文件
|
||
│ │ │ ├── css/ # 游戏样式文件
|
||
│ │ │ ├── images/ # 游戏图片资源
|
||
│ │ │ ├── audio/ # 游戏音频资源
|
||
│ │ │ └── version.js # 游戏版本信息
|
||
│ │ ├── game002/ # 麻将游戏
|
||
│ │ │ ├── index.html
|
||
│ │ │ ├── js/
|
||
│ │ │ ├── css/
|
||
│ │ │ ├── images/
|
||
│ │ │ ├── audio/
|
||
│ │ │ └── version.js
|
||
│ │ └── ... # 其他子游戏
|
||
│ ├── downloads/ # 临时下载目录
|
||
│ │ ├── hall_temp.zip # 临时大厅ZIP包
|
||
│ │ ├── game001_temp.zip # 临时游戏ZIP包
|
||
│ │ └── ...
|
||
│ └── cache/ # 缓存目录
|
||
│ ├── images/ # 图片缓存
|
||
│ ├── config/ # 配置缓存
|
||
│ └── temp/ # 临时文件
|
||
├── config_cache/ # 配置缓存目录
|
||
│ ├── remote_config.json # 远程配置缓存
|
||
│ └── startup_config.json # 启动配置缓存
|
||
└── logs/ # 日志目录
|
||
├── app.log # 应用日志
|
||
├── download.log # 下载日志
|
||
└── error.log # 错误日志
|
||
```
|
||
|
||
### 4.2 ZIP包解压路径规范
|
||
|
||
#### 路径管理类
|
||
|
||
```typescript
|
||
export class PathManager {
|
||
|
||
/**
|
||
* 获取应用根目录路径
|
||
*/
|
||
public static getAppRootPath(): string {
|
||
return getContext().filesDir;
|
||
}
|
||
|
||
/**
|
||
* 获取游戏资源根目录路径
|
||
*/
|
||
public static getGameResourcesPath(): string {
|
||
return `${this.getAppRootPath()}/game_resources`;
|
||
}
|
||
|
||
/**
|
||
* 获取大厅资源目录路径
|
||
*/
|
||
public static getHallResourcePath(): string {
|
||
return `${this.getGameResourcesPath()}/hall`;
|
||
}
|
||
|
||
/**
|
||
* 获取子游戏资源目录路径
|
||
* @param gameId 游戏ID
|
||
*/
|
||
public static getGameResourcePath(gameId: string): string {
|
||
return `${this.getGameResourcesPath()}/games/${gameId}`;
|
||
}
|
||
|
||
/**
|
||
* 获取下载临时目录路径
|
||
*/
|
||
public static getDownloadsPath(): string {
|
||
return `${this.getGameResourcesPath()}/downloads`;
|
||
}
|
||
|
||
/**
|
||
* 获取大厅ZIP包临时下载路径
|
||
*/
|
||
public static getHallZipTempPath(): string {
|
||
return `${this.getDownloadsPath()}/hall_temp.zip`;
|
||
}
|
||
|
||
/**
|
||
* 获取子游戏ZIP包临时下载路径
|
||
* @param gameId 游戏ID
|
||
*/
|
||
public static getGameZipTempPath(gameId: string): string {
|
||
return `${this.getDownloadsPath()}/${gameId}_temp.zip`;
|
||
}
|
||
|
||
/**
|
||
* 获取配置缓存目录路径
|
||
*/
|
||
public static getConfigCachePath(): string {
|
||
return `${this.getAppRootPath()}/config_cache`;
|
||
}
|
||
|
||
/**
|
||
* 获取远程配置缓存文件路径
|
||
*/
|
||
public static getRemoteConfigCachePath(): string {
|
||
return `${this.getConfigCachePath()}/remote_config.json`;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### ZIP包解压流程
|
||
|
||
```typescript
|
||
export class ZipExtractor {
|
||
|
||
/**
|
||
* 解压大厅ZIP包到指定目录
|
||
* @param zipFilePath ZIP包文件路径
|
||
*/
|
||
public static async extractHallZip(zipFilePath: string): Promise<void> {
|
||
const targetPath = PathManager.getHallResourcePath();
|
||
|
||
console.log(`开始解压大厅ZIP包:`);
|
||
console.log(`源文件: ${zipFilePath}`);
|
||
console.log(`目标路径: ${targetPath}`);
|
||
|
||
// 1. 清理目标目录
|
||
await this.cleanDirectory(targetPath);
|
||
|
||
// 2. 创建目标目录
|
||
await fileIo.mkdir(targetPath, true);
|
||
|
||
// 3. 解压ZIP包
|
||
await this.extractZipToDirectory(zipFilePath, targetPath);
|
||
|
||
// 4. 验证解压结果
|
||
await this.verifyExtractedFiles(targetPath);
|
||
|
||
console.log('大厅ZIP包解压完成');
|
||
}
|
||
|
||
/**
|
||
* 解压子游戏ZIP包到指定目录
|
||
* @param zipFilePath ZIP包文件路径
|
||
* @param gameId 游戏ID
|
||
*/
|
||
public static async extractGameZip(zipFilePath: string, gameId: string): Promise<void> {
|
||
const targetPath = PathManager.getGameResourcePath(gameId);
|
||
|
||
console.log(`开始解压游戏ZIP包:`);
|
||
console.log(`游戏ID: ${gameId}`);
|
||
console.log(`源文件: ${zipFilePath}`);
|
||
console.log(`目标路径: ${targetPath}`);
|
||
|
||
// 1. 清理目标目录
|
||
await this.cleanDirectory(targetPath);
|
||
|
||
// 2. 创建目标目录
|
||
await fileIo.mkdir(targetPath, true);
|
||
|
||
// 3. 解压ZIP包
|
||
await this.extractZipToDirectory(zipFilePath, targetPath);
|
||
|
||
// 4. 验证解压结果
|
||
await this.verifyExtractedFiles(targetPath);
|
||
|
||
console.log(`游戏${gameId}的ZIP包解压完成`);
|
||
}
|
||
|
||
/**
|
||
* 清理目录
|
||
*/
|
||
private static async cleanDirectory(dirPath: string): Promise<void> {
|
||
if (await fileIo.access(dirPath)) {
|
||
await fileIo.rmdir(dirPath, true);
|
||
console.log(`已清理目录: ${dirPath}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行ZIP包解压
|
||
*/
|
||
private static async extractZipToDirectory(zipPath: string, targetPath: string): Promise<void> {
|
||
// 使用HarmonyOS的解压API
|
||
const zipFile = new zlib.ZipFile(zipPath);
|
||
await zipFile.extractAll(targetPath);
|
||
|
||
console.log(`ZIP包解压完成: ${zipPath} -> ${targetPath}`);
|
||
}
|
||
|
||
/**
|
||
* 验证解压后的文件
|
||
*/
|
||
private static async verifyExtractedFiles(dirPath: string): Promise<void> {
|
||
// 检查必要的文件是否存在
|
||
const indexHtmlPath = `${dirPath}/index.html`;
|
||
if (!await fileIo.access(indexHtmlPath)) {
|
||
throw new Error(`解压验证失败: 缺少index.html文件`);
|
||
}
|
||
|
||
console.log(`解压文件验证通过: ${dirPath}`);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 大厅启动逻辑
|
||
|
||
### 5.1 大厅资源初始化
|
||
|
||
#### 大厅管理器
|
||
|
||
```typescript
|
||
export class HallManager {
|
||
|
||
/**
|
||
* 初始化大厅资源
|
||
*/
|
||
public static async initializeHall(): Promise<void> {
|
||
try {
|
||
console.log('开始初始化大厅资源');
|
||
|
||
// 1. 检查大厅资源版本
|
||
const needUpdate = await this.checkHallVersion();
|
||
|
||
if (needUpdate) {
|
||
// 2. 下载大厅资源
|
||
await this.downloadHallResources();
|
||
|
||
// 3. 解压大厅资源
|
||
await this.extractHallResources();
|
||
}
|
||
|
||
// 4. 验证大厅资源完整性
|
||
await this.verifyHallResources();
|
||
|
||
// 5. 初始化大厅WebView
|
||
await this.initializeHallWebView();
|
||
|
||
console.log('大厅资源初始化完成');
|
||
|
||
} catch (error) {
|
||
console.error('大厅资源初始化失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查大厅版本
|
||
*/
|
||
private static async checkHallVersion(): Promise<boolean> {
|
||
const config = await ConfigManager.getRemoteConfig();
|
||
const gameData = await ConfigManager.getCurrentGameData();
|
||
|
||
// 获取远程大厅版本
|
||
const remoteVersion = ConfigParameterHelper.getGameVersion(config, gameData);
|
||
console.log('远程大厅版本:', remoteVersion);
|
||
|
||
// 获取本地大厅版本
|
||
const localVersion = await this.getLocalHallVersion();
|
||
console.log('本地大厅版本:', localVersion);
|
||
|
||
// 比较版本
|
||
const needUpdate = VersionComparator.isVersionNewer(remoteVersion, localVersion);
|
||
console.log('大厅需要更新:', needUpdate);
|
||
|
||
return needUpdate;
|
||
}
|
||
|
||
/**
|
||
* 下载大厅资源
|
||
*/
|
||
private static async downloadHallResources(): Promise<void> {
|
||
const config = await ConfigManager.getRemoteConfig();
|
||
const gameData = await ConfigManager.getCurrentGameData();
|
||
|
||
// 获取大厅下载URL
|
||
const downloadUrl = ConfigParameterHelper.getGameDownloadUrl(config, gameData);
|
||
console.log('大厅下载URL:', downloadUrl);
|
||
|
||
if (!downloadUrl) {
|
||
throw new Error('未找到大厅下载URL');
|
||
}
|
||
|
||
// 下载大厅ZIP包
|
||
const zipPath = PathManager.getHallZipTempPath();
|
||
await FileDownloader.download(downloadUrl, zipPath, {
|
||
onProgress: (progress: number, total: number) => {
|
||
const percent = Math.floor((progress / total) * 100);
|
||
console.log(`大厅下载进度: ${percent}%`);
|
||
}
|
||
});
|
||
|
||
console.log('大厅ZIP包下载完成:', zipPath);
|
||
}
|
||
|
||
/**
|
||
* 解压大厅资源
|
||
*/
|
||
private static async extractHallResources(): Promise<void> {
|
||
const zipPath = PathManager.getHallZipTempPath();
|
||
|
||
// 解压到大厅目录
|
||
await ZipExtractor.extractHallZip(zipPath);
|
||
|
||
// 清理临时ZIP文件
|
||
await fileIo.unlink(zipPath);
|
||
|
||
console.log('大厅资源解压完成');
|
||
}
|
||
|
||
/**
|
||
* 获取本地大厅版本
|
||
*/
|
||
private static async getLocalHallVersion(): Promise<string> {
|
||
const versionPath = `${PathManager.getHallResourcePath()}/version.txt`;
|
||
|
||
try {
|
||
if (await fileIo.access(versionPath)) {
|
||
const versionContent = await fileIo.readText(versionPath);
|
||
return versionContent.trim();
|
||
}
|
||
} catch (error) {
|
||
console.warn('读取本地大厅版本失败:', error);
|
||
}
|
||
|
||
return '';
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 大厅WebView启动
|
||
|
||
#### WebView管理器
|
||
|
||
```typescript
|
||
export class HallWebViewController {
|
||
private webView: WebviewController | null = null;
|
||
|
||
/**
|
||
* 初始化大厅WebView
|
||
*/
|
||
public async initializeHallWebView(): Promise<void> {
|
||
try {
|
||
console.log('开始初始化大厅WebView');
|
||
|
||
// 1. 创建WebView控制器
|
||
this.webView = new webview.WebviewController();
|
||
|
||
// 2. 配置WebView设置
|
||
await this.configureWebViewSettings();
|
||
|
||
// 3. 注册JS Bridge接口
|
||
await this.registerJSBridgeHandlers();
|
||
|
||
// 4. 加载大厅HTML页面
|
||
await this.loadHallPage();
|
||
|
||
console.log('大厅WebView初始化完成');
|
||
|
||
} catch (error) {
|
||
console.error('大厅WebView初始化失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 配置WebView设置
|
||
*/
|
||
private async configureWebViewSettings(): Promise<void> {
|
||
if (!this.webView) return;
|
||
|
||
// 启用JavaScript
|
||
this.webView.enableJavaScript = true;
|
||
|
||
// 启用DOM存储
|
||
this.webView.domStorageAccess = true;
|
||
|
||
// 启用文件访问
|
||
this.webView.fileAccess = true;
|
||
|
||
// 设置用户代理
|
||
this.webView.userAgent = 'TSGame_HarmonyOS/3.6.0 (HarmonyOS)';
|
||
|
||
console.log('WebView设置配置完成');
|
||
}
|
||
|
||
/**
|
||
* 注册JS Bridge接口
|
||
*/
|
||
private async registerJSBridgeHandlers(): Promise<void> {
|
||
if (!this.webView) return;
|
||
|
||
// 注册所有JS Bridge接口
|
||
await JSBridgeManager.registerAllHandlers(this.webView);
|
||
|
||
console.log('JS Bridge接口注册完成');
|
||
}
|
||
|
||
/**
|
||
* 加载大厅HTML页面
|
||
*/
|
||
private async loadHallPage(): Promise<void> {
|
||
if (!this.webView) return;
|
||
|
||
const hallIndexPath = `${PathManager.getHallResourcePath()}/index.html`;
|
||
|
||
// 检查大厅HTML文件是否存在
|
||
if (!await fileIo.access(hallIndexPath)) {
|
||
throw new Error('大厅HTML文件不存在');
|
||
}
|
||
|
||
// 构建文件URL
|
||
const fileUrl = `file://${hallIndexPath}`;
|
||
console.log('加载大厅页面:', fileUrl);
|
||
|
||
// 加载页面
|
||
await this.webView.loadUrl(fileUrl);
|
||
|
||
// 设置页面加载监听
|
||
this.webView.onPageBegin = (event) => {
|
||
console.log('大厅页面开始加载:', event.url);
|
||
};
|
||
|
||
this.webView.onPageEnd = (event) => {
|
||
console.log('大厅页面加载完成:', event.url);
|
||
this.onHallPageLoadCompleted();
|
||
};
|
||
|
||
this.webView.onErrorReceive = (event) => {
|
||
console.error('大厅页面加载错误:', event.error);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 大厅页面加载完成回调
|
||
*/
|
||
private onHallPageLoadCompleted(): void {
|
||
console.log('大厅启动完成,可以接受用户操作');
|
||
|
||
// 通知应用大厅已就绪
|
||
EventManager.emit('hall_ready');
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 子游戏跳转启动逻辑
|
||
|
||
### 6.1 子游戏资源管理
|
||
|
||
#### 子游戏启动的完整时序
|
||
|
||
```
|
||
大厅点击游戏
|
||
↓
|
||
HallWebView.callHandler('SwitchOverGameData')
|
||
↓
|
||
GameSwitchController.handleGameSwitch()
|
||
↓
|
||
检查子游戏资源状态 ← 读取 game_version 配置属性
|
||
↓
|
||
[如果需要] 下载子游戏ZIP ← 使用 game_download 配置属性
|
||
↓
|
||
[如果需要] 解压到游戏目录 ← 解压到 /game_resources/games/{gameId}/
|
||
↓
|
||
隐藏大厅WebView,初始化游戏WebView
|
||
↓
|
||
加载游戏页面 ← 加载 file:///game_resources/games/{gameId}/index.html
|
||
↓
|
||
显示游戏WebView,传递启动参数
|
||
↓
|
||
子游戏启动完成
|
||
```
|
||
|
||
#### 子游戏跳转的JSBridge接口调用
|
||
|
||
```typescript
|
||
// 在大厅WebView中的JavaScript代码
|
||
function switchToGame(gameInfo) {
|
||
// 调用SwitchOverGameData接口跳转到子游戏
|
||
window.WebViewJavascriptBridge.callHandler('SwitchOverGameData', {
|
||
gameId: gameInfo.gameId, // 子游戏唯一标识
|
||
gameType: gameInfo.gameType, // 游戏类型
|
||
gameUrl: gameInfo.gameUrl, // 游戏访问地址(用于fallback)
|
||
gameName: gameInfo.gameName, // 游戏显示名称
|
||
gameIcon: gameInfo.gameIcon, // 游戏图标URL
|
||
gameDesc: gameInfo.gameDesc, // 游戏描述
|
||
extraData: { // 传递给子游戏的额外数据
|
||
userId: currentUser.userId,
|
||
userToken: currentUser.token,
|
||
fromHall: true,
|
||
timestamp: Date.now()
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
### 6.2 子游戏WebView切换的详细实现
|
||
|
||
#### WebView切换管理器的完整实现
|
||
|
||
```typescript
|
||
export class GameWebViewController {
|
||
private static hallWebView: WebviewController | null = null;
|
||
private static gameWebViews: Map<string, WebviewController> = new Map();
|
||
private static currentGameId: string = '';
|
||
|
||
/**
|
||
* 处理大厅到子游戏的切换
|
||
* 这是SwitchOverGameData接口的具体实现
|
||
*/
|
||
public static async handleSwitchOverGameData(params: SwitchGameParams): Promise<void> {
|
||
try {
|
||
console.log(`=== 开始处理游戏切换请求 ===`);
|
||
console.log(`游戏ID: ${params.gameId}`);
|
||
console.log(`游戏参数:`, params);
|
||
|
||
// 第一阶段:资源检查和准备
|
||
await this.prepareGameResources(params.gameId, params);
|
||
|
||
// 第二阶段:WebView切换
|
||
await this.performWebViewSwitch(params.gameId, params);
|
||
|
||
// 第三阶段:游戏启动
|
||
await this.launchGameInWebView(params.gameId, params);
|
||
|
||
console.log(`=== 游戏切换完成 ===`);
|
||
|
||
} catch (error) {
|
||
console.error(`游戏切换失败:`, error);
|
||
await this.handleSwitchError(params.gameId, error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 第一阶段:准备游戏资源
|
||
*/
|
||
private static async prepareGameResources(gameId: string, params: SwitchGameParams): Promise<void> {
|
||
console.log(`=== 第一阶段:准备游戏${gameId}资源 ===`);
|
||
|
||
// 1. 获取远程配置,检查版本信息
|
||
const config = await ConfigManager.getRemoteConfig();
|
||
const gameData = await ConfigManager.getCurrentGameData();
|
||
|
||
// 2. 读取游戏版本配置属性进行版本检查
|
||
const remoteGameVersion = ConfigParameterHelper.getGameVersion(config, gameData);
|
||
console.log(`远程游戏版本: ${remoteGameVersion}`);
|
||
|
||
// 3. 获取游戏路径信息
|
||
const gamePaths = GamePathResolver.getGamePaths(gameId);
|
||
console.log(`游戏资源路径: ${gamePaths.extractTargetPath}`);
|
||
console.log(`游戏启动路径: ${gamePaths.webViewLoadUrl}`);
|
||
|
||
// 4. 检查本地游戏资源状态
|
||
const resourceStatus = await this.checkGameResourceStatus(gameId, remoteGameVersion, gamePaths);
|
||
|
||
// 5. 如果需要下载,执行下载和解压流程
|
||
if (resourceStatus.needDownload) {
|
||
await this.downloadAndExtractGame(gameId, config, gameData, gamePaths);
|
||
}
|
||
|
||
console.log(`游戏${gameId}资源准备完成`);
|
||
}
|
||
|
||
/**
|
||
* 检查游戏资源状态的详细逻辑
|
||
*/
|
||
private static async checkGameResourceStatus(
|
||
gameId: string,
|
||
remoteVersion: string,
|
||
gamePaths: GamePathInfo
|
||
): Promise<GameResourceStatus> {
|
||
|
||
// 1. 检查游戏目录是否存在
|
||
const gameExists = await fileIo.access(gamePaths.extractTargetPath);
|
||
if (!gameExists) {
|
||
console.log(`游戏${gameId}目录不存在: ${gamePaths.extractTargetPath}`);
|
||
return { needDownload: true, reason: 'not_exists' };
|
||
}
|
||
|
||
// 2. 检查游戏入口文件
|
||
const entryExists = await fileIo.access(gamePaths.entryFilePath);
|
||
if (!entryExists) {
|
||
console.log(`游戏${gameId}入口文件不存在: ${gamePaths.entryFilePath}`);
|
||
return { needDownload: true, reason: 'entry_missing' };
|
||
}
|
||
|
||
// 3. 读取本地版本信息
|
||
const localVersion = await this.getLocalGameVersion(gamePaths.versionFilePath);
|
||
console.log(`本地游戏版本: ${localVersion}`);
|
||
|
||
// 4. 版本比较
|
||
if (VersionComparator.isVersionNewer(remoteVersion, localVersion)) {
|
||
console.log(`游戏${gameId}需要版本更新: ${localVersion} -> ${remoteVersion}`);
|
||
return {
|
||
needDownload: true,
|
||
reason: 'version_update',
|
||
oldVersion: localVersion,
|
||
newVersion: remoteVersion
|
||
};
|
||
}
|
||
|
||
// 5. 验证关键文件完整性
|
||
const isValid = await this.validateGameFiles(gameId, gamePaths);
|
||
if (!isValid) {
|
||
console.log(`游戏${gameId}文件验证失败`);
|
||
return { needDownload: true, reason: 'file_corruption' };
|
||
}
|
||
|
||
console.log(`游戏${gameId}资源检查通过`);
|
||
return { needDownload: false, reason: 'ready' };
|
||
}
|
||
|
||
/**
|
||
* 下载和解压游戏的详细流程
|
||
*/
|
||
private static async downloadAndExtractGame(
|
||
gameId: string,
|
||
config: any,
|
||
gameData: any,
|
||
gamePaths: GamePathInfo
|
||
): Promise<void> {
|
||
|
||
console.log(`=== 开始下载和解压游戏${gameId} ===`);
|
||
|
||
// 1. 读取游戏下载地址配置属性
|
||
const gameDownloadUrl = DownloadUrlResolver.getGameZipDownloadUrl(config, gameData, gameId);
|
||
console.log(`游戏下载地址: ${gameDownloadUrl}`);
|
||
|
||
if (!gameDownloadUrl) {
|
||
throw new Error(`未找到游戏${gameId}的下载地址配置`);
|
||
}
|
||
|
||
// 2. 显示下载进度界面
|
||
await UIController.showGameDownloadDialog(gameId);
|
||
|
||
try {
|
||
// 3. 下载游戏ZIP包到临时路径
|
||
console.log(`开始下载到: ${gamePaths.downloadTempPath}`);
|
||
await FileDownloader.downloadWithProgress(
|
||
gameDownloadUrl,
|
||
gamePaths.downloadTempPath,
|
||
{
|
||
onProgress: (loaded: number, total: number) => {
|
||
const percent = Math.floor((loaded / total) * 100);
|
||
UIController.updateGameDownloadProgress(gameId, percent);
|
||
console.log(`游戏${gameId}下载进度: ${percent}%`);
|
||
},
|
||
timeout: 300000 // 5分钟超时
|
||
}
|
||
);
|
||
|
||
console.log(`游戏${gameId}下载完成`);
|
||
|
||
// 4. 解压游戏ZIP包到目标路径
|
||
console.log(`开始解压到: ${gamePaths.extractTargetPath}`);
|
||
await GamePathResolver.extractGameZip(gamePaths.downloadTempPath, gameId);
|
||
|
||
console.log(`游戏${gameId}解压完成`);
|
||
|
||
// 5. 清理临时下载文件
|
||
await fileIo.unlink(gamePaths.downloadTempPath);
|
||
console.log(`临时文件已清理: ${gamePaths.downloadTempPath}`);
|
||
|
||
} finally {
|
||
// 6. 隐藏下载进度界面
|
||
await UIController.hideGameDownloadDialog(gameId);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 第二阶段:执行WebView切换
|
||
*/
|
||
private static async performWebViewSwitch(gameId: string, params: SwitchGameParams): Promise<void> {
|
||
console.log(`=== 第二阶段:执行WebView切换到游戏${gameId} ===`);
|
||
|
||
// 1. 保存当前大厅状态
|
||
await this.saveHallState();
|
||
|
||
// 2. 隐藏大厅WebView
|
||
await this.hideHallWebView();
|
||
console.log(`大厅WebView已隐藏`);
|
||
|
||
// 3. 初始化或获取游戏WebView
|
||
const gameWebView = await this.getOrCreateGameWebView(gameId);
|
||
console.log(`游戏${gameId}的WebView已准备`);
|
||
|
||
// 4. 配置游戏WebView
|
||
await this.configureGameWebView(gameId, gameWebView, params);
|
||
console.log(`游戏${gameId}的WebView配置完成`);
|
||
|
||
// 5. 显示游戏WebView
|
||
await this.showGameWebView(gameId);
|
||
console.log(`游戏${gameId}的WebView已显示`);
|
||
|
||
// 6. 更新当前游戏状态
|
||
this.currentGameId = gameId;
|
||
}
|
||
|
||
/**
|
||
* 第三阶段:在WebView中启动游戏
|
||
*/
|
||
private static async launchGameInWebView(gameId: string, params: SwitchGameParams): Promise<void> {
|
||
console.log(`=== 第三阶段:在WebView中启动游戏${gameId} ===`);
|
||
|
||
const gamePaths = GamePathResolver.getGamePaths(gameId);
|
||
const gameWebView = this.gameWebViews.get(gameId);
|
||
|
||
if (!gameWebView) {
|
||
throw new Error(`游戏${gameId}的WebView未找到`);
|
||
}
|
||
|
||
// 1. 加载游戏页面
|
||
console.log(`加载游戏页面: ${gamePaths.webViewLoadUrl}`);
|
||
await gameWebView.loadUrl(gamePaths.webViewLoadUrl);
|
||
|
||
// 2. 等待页面加载完成
|
||
await this.waitForGamePageReady(gameId, gameWebView);
|
||
|
||
// 3. 设置游戏启动参数
|
||
const startupParams = await this.buildGameStartupParams(gameId, params);
|
||
console.log(`游戏启动参数:`, startupParams);
|
||
|
||
// 4. 通过JSBridge传递启动参数
|
||
await this.passStartupParamsToGame(gameId, gameWebView, startupParams);
|
||
|
||
// 5. 通知游戏初始化完成
|
||
await this.notifyGameInitComplete(gameId, gameWebView);
|
||
|
||
console.log(`游戏${gameId}在WebView中启动完成`);
|
||
}
|
||
|
||
/**
|
||
* 获取或创建游戏WebView
|
||
*/
|
||
private static async getOrCreateGameWebView(gameId: string): Promise<WebviewController> {
|
||
let gameWebView = this.gameWebViews.get(gameId);
|
||
|
||
if (!gameWebView) {
|
||
console.log(`创建新的游戏${gameId}WebView`);
|
||
|
||
// 创建新的WebView实例
|
||
gameWebView = new webview.WebviewController();
|
||
|
||
// 基础配置
|
||
gameWebView.setJavaScriptEnabled(true);
|
||
gameWebView.setDomStorageEnabled(true);
|
||
gameWebView.setFileAccessEnabled(true);
|
||
gameWebView.setAllowFileAccessFromFileURLs(true);
|
||
|
||
// 设置用户代理
|
||
const userAgent = `TSGame-HarmonyOS/${await DeviceInfo.getAppVersion()} Game/${gameId}`;
|
||
gameWebView.setUserAgent(userAgent);
|
||
|
||
// 缓存WebView实例
|
||
this.gameWebViews.set(gameId, gameWebView);
|
||
|
||
console.log(`游戏${gameId}WebView创建完成`);
|
||
}
|
||
|
||
return gameWebView;
|
||
}
|
||
|
||
/**
|
||
* 配置游戏WebView的专用设置
|
||
*/
|
||
private static async configureGameWebView(
|
||
gameId: string,
|
||
gameWebView: WebviewController,
|
||
params: SwitchGameParams
|
||
): Promise<void> {
|
||
|
||
// 1. 设置JSBridge
|
||
await JSBridgeManager.setupGameJSBridge(gameId, gameWebView);
|
||
console.log(`游戏${gameId}的JSBridge设置完成`);
|
||
|
||
// 2. 注册游戏专用的Handler
|
||
await this.registerGameSpecificHandlers(gameId, gameWebView);
|
||
|
||
// 3. 设置WebView事件监听
|
||
this.setupGameWebViewListeners(gameId, gameWebView);
|
||
|
||
// 4. 配置游戏特定的WebView选项
|
||
await this.applyGameSpecificWebViewConfig(gameId, gameWebView, params);
|
||
}
|
||
|
||
/**
|
||
* 构建游戏启动参数
|
||
*/
|
||
private static async buildGameStartupParams(gameId: string, params: SwitchGameParams): Promise<any> {
|
||
const config = await ConfigManager.getRemoteConfig();
|
||
const gameData = await ConfigManager.getCurrentGameData();
|
||
const userInfo = await UserManager.getCurrentUserInfo();
|
||
|
||
return {
|
||
// 游戏基础信息
|
||
gameId: gameId,
|
||
gameName: params.gameName,
|
||
gameType: params.gameType,
|
||
|
||
// 用户信息
|
||
userId: userInfo.userId,
|
||
userToken: userInfo.token,
|
||
nickname: userInfo.nickname,
|
||
avatar: userInfo.avatar,
|
||
coins: userInfo.coins,
|
||
level: userInfo.level,
|
||
|
||
// 服务器配置
|
||
serverUrl: ConfigParameterHelper.getServerUrl(config, gameData),
|
||
apiVersion: 'v1.0',
|
||
|
||
// 启动上下文
|
||
fromPage: 'hall',
|
||
timestamp: Date.now(),
|
||
sessionId: generateSessionId(),
|
||
|
||
// 设备信息
|
||
deviceInfo: await DeviceInfo.getBasicInfo(),
|
||
|
||
// 传递的额外数据
|
||
extraData: params.extraData || {}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 通过JSBridge传递启动参数给游戏
|
||
*/
|
||
private static async passStartupParamsToGame(
|
||
gameId: string,
|
||
gameWebView: WebviewController,
|
||
startupParams: any
|
||
): Promise<void> {
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => {
|
||
reject(new Error(`游戏${gameId}启动参数传递超时`));
|
||
}, 10000);
|
||
|
||
// 调用游戏的初始化Handler
|
||
gameWebView.callHandler('onGameInit', startupParams, (response) => {
|
||
clearTimeout(timeout);
|
||
|
||
console.log(`游戏${gameId}初始化响应:`, response);
|
||
|
||
if (response && response.success) {
|
||
resolve();
|
||
} else {
|
||
reject(new Error(`游戏${gameId}初始化失败: ${response?.message || '未知错误'}`));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 等待游戏页面加载完成
|
||
*/
|
||
private static async waitForGamePageReady(gameId: string, gameWebView: WebviewController): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => {
|
||
reject(new Error(`游戏${gameId}页面加载超时`));
|
||
}, 30000);
|
||
|
||
// 监听页面加载完成事件
|
||
gameWebView.setOnPageFinished((url) => {
|
||
clearTimeout(timeout);
|
||
console.log(`游戏${gameId}页面加载完成: ${url}`);
|
||
resolve();
|
||
});
|
||
|
||
// 监听页面加载错误
|
||
gameWebView.setOnPageError((error) => {
|
||
clearTimeout(timeout);
|
||
reject(new Error(`游戏${gameId}页面加载失败: ${error.description}`));
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 隐藏大厅WebView
|
||
*/
|
||
private static async hideHallWebView(): Promise<void> {
|
||
if (this.hallWebView) {
|
||
// 通知大厅页面即将隐藏
|
||
this.hallWebView.callHandler('onPagePause', { reason: 'game_switch' });
|
||
|
||
// 隐藏大厅WebView UI
|
||
await UIController.hideHallWebView();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示游戏WebView
|
||
*/
|
||
private static async showGameWebView(gameId: string): Promise<void> {
|
||
// 显示游戏WebView UI
|
||
await UIController.showGameWebView(gameId);
|
||
|
||
// 通知游戏页面已显示
|
||
const gameWebView = this.gameWebViews.get(gameId);
|
||
if (gameWebView) {
|
||
gameWebView.callHandler('onPageResume', {
|
||
fromBackground: false,
|
||
reason: 'game_start'
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 游戏返回大厅的逻辑
|
||
*/
|
||
public static async switchBackToHall(gameId: string): Promise<void> {
|
||
console.log(`=== 游戏${gameId}返回大厅 ===`);
|
||
|
||
// 1. 隐藏游戏WebView
|
||
await UIController.hideGameWebView(gameId);
|
||
|
||
// 2. 通知游戏页面暂停
|
||
const gameWebView = this.gameWebViews.get(gameId);
|
||
if (gameWebView) {
|
||
gameWebView.callHandler('onPagePause', { reason: 'back_to_hall' });
|
||
}
|
||
|
||
// 3. 显示大厅WebView
|
||
await UIController.showHallWebView();
|
||
|
||
// 4. 通知大厅页面恢复
|
||
if (this.hallWebView) {
|
||
this.hallWebView.callHandler('onPageResume', {
|
||
fromBackground: false,
|
||
reason: 'back_from_game',
|
||
gameId: gameId
|
||
});
|
||
}
|
||
|
||
// 5. 更新当前状态
|
||
this.currentGameId = '';
|
||
|
||
console.log(`已返回大厅`);
|
||
}
|
||
|
||
/**
|
||
* 处理切换错误
|
||
*/
|
||
private static async handleSwitchError(gameId: string, error: Error): Promise<void> {
|
||
console.error(`游戏${gameId}切换失败:`, error);
|
||
|
||
// 记录错误日志
|
||
await Logger.logError('GAME_SWITCH_ERROR', {
|
||
gameId,
|
||
error: error.message,
|
||
stack: error.stack,
|
||
timestamp: Date.now()
|
||
});
|
||
|
||
// 隐藏可能的加载界面
|
||
await UIController.hideGameDownloadDialog(gameId);
|
||
|
||
// 显示错误提示
|
||
await UIController.showErrorDialog(`游戏启动失败`, `${error.message}\n\n请稍后重试或联系客服。`);
|
||
|
||
// 确保回到大厅状态
|
||
await this.switchBackToHall(gameId);
|
||
}
|
||
}
|
||
|
||
// 辅助数据结构
|
||
interface SwitchGameParams {
|
||
gameId: string;
|
||
gameType: string;
|
||
gameUrl: string;
|
||
gameName: string;
|
||
gameIcon: string;
|
||
gameDesc?: string;
|
||
extraData?: any;
|
||
}
|
||
|
||
interface GameResourceStatus {
|
||
needDownload: boolean;
|
||
reason: 'not_exists' | 'entry_missing' | 'version_update' | 'file_corruption' | 'ready';
|
||
oldVersion?: string;
|
||
newVersion?: string;
|
||
}
|
||
```
|
||
|
||
### 6.3 子游戏启动流程的配置属性读取详解
|
||
|
||
#### 启动过程中读取的具体配置属性
|
||
|
||
```typescript
|
||
export class GameStartupConfigReader {
|
||
|
||
/**
|
||
* 子游戏启动过程中需要读取的所有配置属性
|
||
*/
|
||
public static async readGameStartupConfig(gameId: string): Promise<GameStartupConfig> {
|
||
const config = await ConfigManager.getRemoteConfig();
|
||
const gameData = await ConfigManager.getCurrentGameData();
|
||
|
||
console.log(`=== 读取游戏${gameId}启动配置 ===`);
|
||
|
||
return {
|
||
// 版本相关配置(用于资源检查)
|
||
gameVersion: ConfigParameterHelper.getGameVersion(config, gameData),
|
||
|
||
// 下载相关配置(用于资源下载)
|
||
gameDownloadUrl: ConfigParameterHelper.getGameDownloadUrl(config, gameData),
|
||
gameSize: ConfigParameterHelper.getGameSize(config, gameData),
|
||
|
||
// 服务器相关配置(传递给游戏用于网络请求)
|
||
serverUrl: ConfigParameterHelper.getServerUrl(config, gameData),
|
||
apiEndpoint: ConfigParameterHelper.getString(config, gameData, 'api_endpoint', '/api'),
|
||
|
||
// 功能开关配置
|
||
enableAudio: ConfigParameterHelper.getBoolean(config, gameData, 'enable_audio', true),
|
||
enableVibration: ConfigParameterHelper.getBoolean(config, gameData, 'enable_vibration', true),
|
||
|
||
// 游戏专用配置
|
||
maxPlayers: ConfigParameterHelper.getNumber(config, gameData, 'max_players', 4),
|
||
gameTimeout: ConfigParameterHelper.getNumber(config, gameData, 'game_timeout', 300),
|
||
|
||
// 调试配置
|
||
debugMode: ConfigParameterHelper.getBoolean(config, gameData, 'debug_mode', false),
|
||
logLevel: ConfigParameterHelper.getString(config, gameData, 'log_level', 'info')
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 打印配置读取详细信息
|
||
*/
|
||
public static logConfigDetails(gameId: string, config: any, gameData: any): void {
|
||
console.log(`=== 游戏${gameId}配置读取详情 ===`);
|
||
|
||
// 显示配置文件层级结构
|
||
console.log(`配置文件层级信息:`);
|
||
console.log(`- 代理商ID: ${gameData.agentid}`);
|
||
console.log(`- 渠道ID: ${gameData.channelid}`);
|
||
console.log(`- 市场ID: ${gameData.marketid}`);
|
||
|
||
// 显示版本配置读取路径
|
||
console.log(`版本配置读取路径:`);
|
||
this.logConfigPath('game_version', config, gameData);
|
||
|
||
// 显示下载配置读取路径
|
||
console.log(`下载配置读取路径:`);
|
||
this.logConfigPath('game_download', config, gameData);
|
||
|
||
// 显示服务器配置读取路径
|
||
console.log(`服务器配置读取路径:`);
|
||
this.logConfigPath('url', config, gameData);
|
||
}
|
||
|
||
/**
|
||
* 记录具体配置属性的读取路径
|
||
*/
|
||
private static logConfigPath(propertyName: string, config: any, gameData: any): void {
|
||
const paths = [
|
||
`marketlist[${gameData.marketid}].${propertyName}`,
|
||
`channellist[${gameData.channelid}].${propertyName}`,
|
||
`agentlist[${gameData.agentid}].${propertyName}`,
|
||
`data.${propertyName}`
|
||
];
|
||
|
||
for (const path of paths) {
|
||
const value = this.getValueByPath(config, path);
|
||
if (value !== undefined) {
|
||
console.log(` ✓ ${path} = ${value}`);
|
||
break;
|
||
} else {
|
||
console.log(` ✗ ${path} = undefined`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据路径获取配置值
|
||
*/
|
||
private static getValueByPath(config: any, path: string): any {
|
||
const parts = path.split('.');
|
||
let current = config;
|
||
|
||
for (const part of parts) {
|
||
if (part.includes('[') && part.includes(']')) {
|
||
// 处理数组索引,如 marketlist[market001]
|
||
const arrayName = part.substring(0, part.indexOf('['));
|
||
const index = part.substring(part.indexOf('[') + 1, part.indexOf(']'));
|
||
|
||
if (current[arrayName] && Array.isArray(current[arrayName])) {
|
||
current = current[arrayName].find((item: any) =>
|
||
item.marketid === index || item.channelid === index || item.agentid === index
|
||
);
|
||
} else {
|
||
return undefined;
|
||
}
|
||
} else {
|
||
current = current[part];
|
||
}
|
||
|
||
if (current === undefined) {
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
return current;
|
||
}
|
||
}
|
||
|
||
interface GameStartupConfig {
|
||
gameVersion: string;
|
||
gameDownloadUrl: string;
|
||
gameSize: string;
|
||
serverUrl: string;
|
||
apiEndpoint: string;
|
||
enableAudio: boolean;
|
||
enableVibration: boolean;
|
||
maxPlayers: number;
|
||
gameTimeout: number;
|
||
debugMode: boolean;
|
||
logLevel: string;
|
||
}
|
||
```
|
||
|
||
### 6.4 子游戏启动路径的完整解析
|
||
|
||
#### 游戏启动路径的构建和验证
|
||
|
||
```typescript
|
||
export class GameLaunchPathResolver {
|
||
|
||
/**
|
||
* 解析游戏的完整启动路径
|
||
*/
|
||
public static resolveGameLaunchPaths(gameId: string): GameLaunchPaths {
|
||
const appRoot = getContext().filesDir;
|
||
|
||
const paths = {
|
||
// 基础路径
|
||
appRootPath: appRoot,
|
||
gameRootPath: `${appRoot}/game_resources/games/${gameId}`,
|
||
|
||
// 启动相关路径
|
||
entryHtmlPath: `${appRoot}/game_resources/games/${gameId}/index.html`,
|
||
webViewLoadUrl: `file://${appRoot}/game_resources/games/${gameId}/index.html`,
|
||
|
||
// 资源路径
|
||
jsPath: `${appRoot}/game_resources/games/${gameId}/js`,
|
||
cssPath: `${appRoot}/game_resources/games/${gameId}/css`,
|
||
imagesPath: `${appRoot}/game_resources/games/${gameId}/images`,
|
||
audioPath: `${appRoot}/game_resources/games/${gameId}/audio`,
|
||
|
||
// 配置和数据路径
|
||
gameConfigPath: `${appRoot}/game_resources/games/${gameId}/game.json`,
|
||
versionFilePath: `${appRoot}/game_resources/games/${gameId}/version.txt`,
|
||
saveDataPath: `${appRoot}/game_resources/games/${gameId}/savedata`,
|
||
|
||
// 临时和缓存路径
|
||
tempPath: `${appRoot}/game_resources/games/${gameId}/temp`,
|
||
cachePath: `${appRoot}/game_resources/games/${gameId}/cache`
|
||
};
|
||
|
||
console.log(`=== 游戏${gameId}启动路径解析 ===`);
|
||
console.log(`游戏根目录: ${paths.gameRootPath}`);
|
||
console.log(`启动入口: ${paths.entryHtmlPath}`);
|
||
console.log(`WebView加载URL: ${paths.webViewLoadUrl}`);
|
||
|
||
return paths;
|
||
}
|
||
|
||
/**
|
||
* 验证游戏启动路径的有效性
|
||
*/
|
||
public static async validateGameLaunchPaths(gameId: string, paths: GameLaunchPaths): Promise<ValidationResult> {
|
||
console.log(`=== 验证游戏${gameId}启动路径 ===`);
|
||
|
||
const validationResult: ValidationResult = {
|
||
isValid: true,
|
||
errors: [],
|
||
warnings: []
|
||
};
|
||
|
||
// 1. 验证游戏根目录
|
||
if (!await fileIo.access(paths.gameRootPath)) {
|
||
validationResult.isValid = false;
|
||
validationResult.errors.push(`游戏根目录不存在: ${paths.gameRootPath}`);
|
||
}
|
||
|
||
// 2. 验证启动入口文件
|
||
if (!await fileIo.access(paths.entryHtmlPath)) {
|
||
validationResult.isValid = false;
|
||
validationResult.errors.push(`游戏入口文件不存在: ${paths.entryHtmlPath}`);
|
||
}
|
||
|
||
// 3. 验证关键资源目录
|
||
const requiredDirs = [paths.jsPath, paths.cssPath];
|
||
for (const dir of requiredDirs) {
|
||
if (!await fileIo.access(dir)) {
|
||
validationResult.warnings.push(`资源目录不存在: ${dir}`);
|
||
}
|
||
}
|
||
|
||
// 4. 验证版本文件
|
||
if (!await fileIo.access(paths.versionFilePath)) {
|
||
validationResult.warnings.push(`版本文件不存在: ${paths.versionFilePath}`);
|
||
}
|
||
|
||
console.log(`游戏${gameId}路径验证结果:`, validationResult);
|
||
return validationResult;
|
||
}
|
||
|
||
/**
|
||
* 创建游戏启动所需的目录结构
|
||
*/
|
||
public static async createGameDirectoryStructure(gameId: string, paths: GameLaunchPaths): Promise<void> {
|
||
console.log(`=== 创建游戏${gameId}目录结构 ===`);
|
||
|
||
const directories = [
|
||
paths.gameRootPath,
|
||
paths.jsPath,
|
||
paths.cssPath,
|
||
paths.imagesPath,
|
||
paths.audioPath,
|
||
paths.saveDataPath,
|
||
paths.tempPath,
|
||
paths.cachePath
|
||
];
|
||
|
||
for (const dir of directories) {
|
||
try {
|
||
await fileIo.mkdir(dir, true);
|
||
console.log(`目录创建成功: ${dir}`);
|
||
} catch (error) {
|
||
console.warn(`目录创建失败: ${dir}`, error);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
interface GameLaunchPaths {
|
||
appRootPath: string;
|
||
gameRootPath: string;
|
||
entryHtmlPath: string;
|
||
webViewLoadUrl: string;
|
||
jsPath: string;
|
||
cssPath: string;
|
||
imagesPath: string;
|
||
audioPath: string;
|
||
gameConfigPath: string;
|
||
versionFilePath: string;
|
||
saveDataPath: string;
|
||
tempPath: string;
|
||
cachePath: string;
|
||
}
|
||
|
||
interface ValidationResult {
|
||
isValid: boolean;
|
||
errors: string[];
|
||
warnings: string[];
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
TSGame HarmonyOS应用的启动流程采用分阶段、可恢复的设计理念,通过精确的配置属性读取和路径管理实现应用和游戏的动态更新。
|
||
|
||
### 关键配置属性汇总
|
||
|
||
#### 应用升级验证的配置属性
|
||
|
||
| 配置属性 | 读取方法 | 作用说明 | 使用时机 |
|
||
|---------|----------|----------|----------|
|
||
| `app_version` | `ConfigParameterHelper.getAppVersion()` | **主要验证属性** - 远程应用版本号,与本地版本比较判断是否需要升级 | 应用启动时版本检查 |
|
||
| `app_download` | `ConfigParameterHelper.getAppDownloadUrl()` | **下载地址属性** - 应用APK/APP安装包下载URL | 确认需要升级时获取下载地址 |
|
||
| `app_size` | `ConfigParameterHelper.getAppSize()` | 应用安装包大小,用于显示下载进度和预估时间 | 下载过程中显示进度 |
|
||
| `force_update` | `ConfigParameterHelper.getBooleanValue()` | 强制更新标志,控制是否必须升级 | 版本检查时确定升级策略 |
|
||
| `min_version` | `ConfigParameterHelper.getString()` | 最低支持版本,低于此版本强制升级 | 版本检查时确定升级策略 |
|
||
| `showmessage` | `ConfigParameterHelper.getShowMessage()` | 版本更新说明,向用户展示升级内容 | 升级提示对话框显示 |
|
||
|
||
#### 游戏资源管理的配置属性
|
||
|
||
| 配置属性 | 读取方法 | 作用说明 | 使用时机 |
|
||
|---------|----------|----------|----------|
|
||
| `game_version` | `ConfigParameterHelper.getGameVersion()` | **主要验证属性** - 远程游戏资源版本号,与本地比较判断是否需要更新 | 游戏启动前资源检查 |
|
||
| `game_download` | `ConfigParameterHelper.getGameDownloadUrl()` | **下载地址属性** - 游戏ZIP包基础下载URL,拼接gameId构成完整下载地址 | 需要下载游戏资源时 |
|
||
| `game_size` | `ConfigParameterHelper.getGameSize()` | 游戏ZIP包大小,用于下载进度显示 | 游戏资源下载过程中 |
|
||
|
||
#### 服务器和功能配置属性
|
||
|
||
| 配置属性 | 读取方法 | 作用说明 | 使用时机 |
|
||
|---------|----------|----------|----------|
|
||
| `url` | `ConfigParameterHelper.getServerUrl()` | **服务器地址属性** - API服务器基础URL,传递给游戏用于网络请求 | 游戏启动时传递给WebView |
|
||
| `api_endpoint` | `ConfigParameterHelper.getString()` | API端点路径,拼接到服务器地址后 | 构建完整API地址 |
|
||
| `enable_audio` | `ConfigParameterHelper.getBoolean()` | 音频功能开关,控制游戏是否启用音频 | 游戏初始化时配置 |
|
||
| `enable_vibration` | `ConfigParameterHelper.getBoolean()` | 震动功能开关,控制游戏是否启用震动反馈 | 游戏初始化时配置 |
|
||
|
||
### 关键路径汇总
|
||
|
||
#### 大厅相关路径
|
||
|
||
| 路径类型 | 具体路径 | 用途说明 |
|
||
|---------|----------|----------|
|
||
| **大厅ZIP下载临时路径** | `{filesDir}/downloads/hall_temp.zip` | 大厅ZIP包下载的临时存储位置 |
|
||
| **大厅资源解压目标路径** | `{filesDir}/game_resources/hall/` | 大厅ZIP包解压后的资源存放目录 |
|
||
| **大厅启动入口路径** | `{filesDir}/game_resources/hall/index.html` | 大厅WebView加载的HTML入口文件 |
|
||
| **大厅WebView加载URL** | `file://{filesDir}/game_resources/hall/index.html` | 大厅WebView实际加载的file://协议URL |
|
||
|
||
#### 子游戏相关路径
|
||
|
||
| 路径类型 | 具体路径模板 | 用途说明 |
|
||
|---------|-------------|----------|
|
||
| **子游戏ZIP下载临时路径** | `{filesDir}/downloads/{gameId}_temp.zip` | 子游戏ZIP包下载的临时存储位置 |
|
||
| **子游戏资源解压目标路径** | `{filesDir}/game_resources/games/{gameId}/` | 子游戏ZIP包解压后的资源存放目录 |
|
||
| **子游戏启动入口路径** | `{filesDir}/game_resources/games/{gameId}/index.html` | 子游戏WebView加载的HTML入口文件 |
|
||
| **子游戏WebView加载URL** | `file://{filesDir}/game_resources/games/{gameId}/index.html` | 子游戏WebView实际加载的file://协议URL |
|
||
| **子游戏JS资源路径** | `{filesDir}/game_resources/games/{gameId}/js/` | 子游戏JavaScript文件存放目录 |
|
||
| **子游戏CSS资源路径** | `{filesDir}/game_resources/games/{gameId}/css/` | 子游戏CSS样式文件存放目录 |
|
||
| **子游戏图片资源路径** | `{filesDir}/game_resources/games/{gameId}/images/` | 子游戏图片资源存放目录 |
|
||
| **子游戏音频资源路径** | `{filesDir}/game_resources/games/{gameId}/audio/` | 子游戏音频文件存放目录 |
|
||
|
||
#### 配置和缓存路径
|
||
|
||
| 路径类型 | 具体路径 | 用途说明 |
|
||
|---------|----------|----------|
|
||
| **远程配置缓存路径** | `{filesDir}/config_cache/remote_config.json` | 远程配置文件的本地缓存 |
|
||
| **启动配置缓存路径** | `{filesDir}/config_cache/startup_config.json` | 应用启动配置的本地缓存 |
|
||
|
||
### 大厅跳转子游戏的完整流程
|
||
|
||
#### 第一阶段:接收跳转请求
|
||
1. **触发方式**:大厅WebView中JavaScript调用 `window.WebViewJavascriptBridge.callHandler('SwitchOverGameData', {...})`
|
||
2. **参数验证**:验证gameId、gameName等必要参数
|
||
3. **显示加载**:显示游戏切换加载界面
|
||
|
||
#### 第二阶段:资源检查和准备
|
||
1. **读取配置**:
|
||
- 读取 `game_version` 属性进行版本比较
|
||
- 读取 `game_download` 属性获取下载地址
|
||
- 读取 `game_size` 属性用于进度显示
|
||
|
||
2. **资源检查**:
|
||
- 检查游戏目录:`{filesDir}/game_resources/games/{gameId}/`
|
||
- 检查入口文件:`{filesDir}/game_resources/games/{gameId}/index.html`
|
||
- 读取本地版本:`{filesDir}/game_resources/games/{gameId}/version.txt`
|
||
|
||
3. **下载解压**(如需要):
|
||
- 下载到临时路径:`{filesDir}/downloads/{gameId}_temp.zip`
|
||
- 解压到目标路径:`{filesDir}/game_resources/games/{gameId}/`
|
||
- 清理临时文件
|
||
|
||
#### 第三阶段:WebView切换
|
||
1. **隐藏大厅**:调用大厅WebView的 `onPagePause` 事件
|
||
2. **初始化游戏WebView**:创建或获取gameId对应的WebView实例
|
||
3. **配置JSBridge**:设置游戏专用的JavaScript Bridge接口
|
||
4. **显示游戏WebView**:将游戏WebView设置为可见状态
|
||
|
||
#### 第四阶段:游戏启动
|
||
1. **加载游戏页面**:WebView加载 `file://{filesDir}/game_resources/games/{gameId}/index.html`
|
||
2. **传递启动参数**:
|
||
- 用户信息(userId、token、coins等)
|
||
- 服务器配置(从 `url` 属性读取)
|
||
- 游戏配置(从其他配置属性读取)
|
||
3. **调用游戏初始化**:通过JSBridge调用游戏的 `onGameInit` 接口
|
||
4. **隐藏加载界面**:游戏初始化完成后隐藏加载界面
|
||
|
||
### 配置属性读取的层级优先级
|
||
|
||
TSGame应用的配置读取遵循严格的层级优先级规则:
|
||
|
||
```
|
||
市场级别配置 (最高优先级)
|
||
↓ 覆盖
|
||
渠道级别配置
|
||
↓ 覆盖
|
||
代理商级别配置
|
||
↓ 覆盖
|
||
全局级别配置 (最低优先级,默认值)
|
||
```
|
||
|
||
#### 具体的配置路径读取顺序
|
||
|
||
以 `app_version` 配置属性为例:
|
||
|
||
1. **第一优先级**:`marketlist[{marketid}].app_version`
|
||
2. **第二优先级**:`channellist[{channelid}].app_version`
|
||
3. **第三优先级**:`agentlist[{agentid}].app_version`
|
||
4. **第四优先级**:`data.app_version`(全局默认值)
|
||
|
||
### 核心特性总结
|
||
|
||
1. **配置驱动启动**:所有启动决策基于远程配置文件的具体属性值
|
||
2. **分层配置管理**:支持全局、代理商、渠道、市场四级配置覆盖机制
|
||
3. **精确路径管理**:每个资源都有明确的下载路径、解压路径和启动路径
|
||
4. **双WebView架构**:大厅和子游戏完全独立的WebView实例和资源管理
|
||
5. **版本检查机制**:基于 `app_version` 和 `game_version` 的自动版本检测
|
||
6. **异常降级处理**:配置获取失败、资源下载失败等多重降级策略
|
||
|
||
### 开发者要点
|
||
|
||
1. **配置属性命名**:所有配置属性名称必须与Android版本保持一致
|
||
2. **路径兼容性**:文件路径需要适配HarmonyOS的沙盒文件系统
|
||
3. **权限申请**:文件读写、网络访问等权限需要动态申请
|
||
4. **性能优化**:WebView初始化、资源解压等耗时操作需要异步处理
|
||
5. **错误处理**:每个环节都需要完善的错误处理和用户提示机制
|
||
|
||
这个启动流程设计确保了TSGame应用在HarmonyOS平台上的高可用性、可维护性和用户体验的一致性。通过精确的配置管理和路径规范,实现了与Android版本完全一致的功能特性。
|