Files
youle_app_ohos/entry/src/main/ets/pages/BridgeGameContainer.ets
T
lanterngamescnandClaude Opus 4.8 01e4c0e99e M3 补全:移除 EchoSample 样板 + getcompareCode 接版本决策 + app_gamename
- 移除 EchoSampleProvider(组装根/导出/文件):M3 全部 Provider 已就位,
  getTime 由 DeviceProvider 提供;样板回显仅 M1 测试页用,生产无依赖
- getcompareCode:启动期 StartupOrchestrator 把网络 App 版本(VersionDecision.appVersion)
  存 KvStore(KV_KEY_NET_APP_VERSION),DeviceProvider 同步读出与本地 config.appversion 比较,
  本地>网络→'1' 否则'0'(对齐 Android apputil.code 语义)
- app_gamename:VersionXml.parse 解析子 <game name>;ResourceManager.localGameName 暴露;
  AppDataInjector.buildValues 接 gameName 参数;大厅/子游戏两处注入均填入

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:33:13 +08:00

220 lines
9.8 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { webview } from '@kit.ArkWeb';
import { common } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { ConfigManager, ResourceManager, AppDataInjector, AppDataValues } from 'domain_resource';
import { KvStore, LocalUploadServer } from 'platform';
import { OutboundHandlers } from 'contracts';
import { AppEnv, EventBus, EventPayload, NavEvents, OpenGenericWebPayload, SwitchGamePayload,
BackGamePayload, Logger } from 'common';
import { BridgeGameParams, GenericWebParams, GenericWebResult, RouteName, AppEvents } from '../routes/AppRoutes';
import { WebSlot } from '../web/WebSlot';
/**
* 大厅/子游戏容器(对应 webviewActivity,框架 §7.1/§7.5)。**双 Web 槽模型**
* - 大厅 Web 常驻(始终在 Stack);子游戏 Web 进入创建/返回销毁(subgameUrl 空/非空驱动)。
* - 同时只有一个激活:进子游戏 → 大厅 deactivate(appservice'2'+停能力+桥关+onInactive+隐藏)、子游戏 activate
* 返回大厅 → 销毁子游戏、大厅 resumeLoaded(appservice'1'+getWebdata)。
* - 每槽独立桥+能力(结构隔离,接口不串);cookie 全局共享、localStorage file:// 同源共享(已验证)。
*/
@Component
export struct BridgeGameContainer {
pathStack: NavPathStack = new NavPathStack();
params: BridgeGameParams = { entryUrl: '', launchType: '0' };
private controllerL: webview.WebviewController = new webview.WebviewController();
// 子游戏 controller 不复用:每次进子游戏新建(随 slotS),避免复用同一 controller 时
// onControllerAttached 不再触发导致第二个子游戏建不出桥(review 发现)。存于 slotS.controller。
private uploadServer: LocalUploadServer = new LocalUploadServer();
private hostCtx: common.UIAbilityContext | undefined = undefined;
private config: ConfigManager | undefined = undefined;
private kv: KvStore | undefined = undefined;
private resource: ResourceManager | undefined = undefined;
private slotL: WebSlot | undefined = undefined;
private slotS: WebSlot | undefined = undefined;
private cancels: Array<() => void> = [];
/** 非空 → 子游戏 Web 显示(路径 A 切换)。 */
@State private subgameUrl: string = '';
aboutToAppear(): void {
if (AppEnv.isDebug()) {
webview.WebviewController.setWebDebuggingAccess(true);
}
}
aboutToDisappear(): void {
// 先退订事件,再 dispose——避免销毁期 SWITCH_GAME/FOREGROUND 等事件又触发建槽/回调
for (const cancel of this.cancels) {
cancel();
}
this.cancels = [];
this.slotS?.dispose();
this.slotL?.dispose();
this.uploadServer.stop();
this.slotS = undefined;
this.slotL = undefined;
this.hostCtx = undefined;
this.config = undefined;
this.kv = undefined;
this.resource = undefined;
}
/** 懒初始化共享依赖(两槽共用 config/kv/resource+ 订阅事件。 */
private ensureDeps(): void {
if (this.hostCtx !== undefined) {
return;
}
const hostCtx: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
this.hostCtx = hostCtx;
this.config = ConfigManager.load(hostCtx);
this.kv = KvStore.create(hostCtx);
this.resource = new ResourceManager(hostCtx, this.config.getLocal(), this.kv);
this.uploadServer.start();
this.subscribeEvents();
}
private resourceRoot(): string {
return ResourceManager.resourceRoot(this.hostCtx as common.UIAbilityContext);
}
private setupLobby(): void {
this.ensureDeps();
if (this.slotL === undefined) {
this.slotL = new WebSlot('lobby', this.controllerL, true, this.uploadServer);
}
this.slotL.setup(this.hostCtx!, this.config!, this.kv!, this.resourceRoot());
}
private setupSubgame(): void {
this.ensureDeps();
this.slotS?.setup(this.hostCtx!, this.config!, this.kv!, this.resourceRoot());
}
private activeSlot(): WebSlot | undefined {
return this.subgameUrl !== '' ? this.slotS : this.slotL;
}
private subscribeEvents(): void {
this.cancels.push(EventBus.on(AppEvents.FOREGROUND, () => this.activeSlot()?.onAppForeground()));
this.cancels.push(EventBus.on(AppEvents.BACKGROUND, () => this.activeSlot()?.onAppBackground()));
this.cancels.push(EventBus.on(NavEvents.OPEN_GENERIC_WEB, (p?: EventPayload) => this.openGenericWeb(p)));
this.cancels.push(EventBus.on(NavEvents.SWITCH_GAME, (p?: EventPayload) => this.switchGame(p)));
this.cancels.push(EventBus.on(NavEvents.BACK_GAME, (p?: EventPayload) => this.backToLobby(p)));
}
/** 路径 B:打开通用网页容器;关闭回传 → 当前激活槽 callHandler('getWebdata')。 */
private openGenericWeb(p?: EventPayload): void {
if (p === undefined) {
return;
}
const d = p as OpenGenericWebPayload;
const params: GenericWebParams = { url: d.url, title: d.title, data: d.data, orientation: d.orientation };
this.pathStack.pushPathByName(RouteName.GENERIC_WEB, params, (info: PopInfo) => {
const result = info.result as GenericWebResult;
this.activeSlot()?.bridge()?.callHandler(OutboundHandlers.GetWebData, result !== undefined ? result.data : '');
}, false);
}
/** 路径 A:大厅 → 子游戏(大厅去激活并保活,创建子游戏 Web)。 */
private switchGame(p?: EventPayload): void {
if (p === undefined) {
return;
}
// 拓扑约束:仅 大厅↔子游戏,无 子游戏→子游戏。已在子游戏中时忽略——
// 游戏逻辑须先 backgameData 回大厅、再由大厅发 SwitchOverGameData 进新子游戏。
if (this.subgameUrl !== '') {
Logger.tag('BridgeGameContainer').w('SwitchOverGameData ignored: already in subgame (route via lobby)');
return;
}
const d = p as SwitchGamePayload;
const res = this.resource;
const cfg = this.config;
if (d.dir === '' || res === undefined || cfg === undefined) {
return;
}
const subDir: string = `${res.paths().urlpath}/${d.dir}`;
// 子游戏目录自带 app_data.jsH5 同步读),launchtype='1'
const values: AppDataValues = AppDataInjector.buildValues(cfg.getLocal(), `${res.localVersion()}`, '1', res.localGameName());
AppDataInjector.inject(subDir, values);
this.slotL?.deactivate();
this.slotS = new WebSlot('subgame', new webview.WebviewController(), true, this.uploadServer);
this.applyOrientation(d.webtype !== '2'); // "3" 横 / "2" 竖
this.subgameUrl = `file://${subDir}/index.html?Launchtype=1`; // 触发子游戏 Web 渲染 → setupSubgame
}
/** 子游戏 → 大厅(销毁子游戏 Web,大厅恢复并带回数据)。 */
private backToLobby(p?: EventPayload): void {
if (this.subgameUrl === '') {
return;
}
const data: string = p !== undefined ? (p as BackGamePayload).data : '';
this.slotS?.dispose();
this.slotS = undefined;
this.subgameUrl = ''; // 移除子游戏 Web
this.applyOrientation(true); // 大厅横屏
if (this.slotL !== undefined) {
this.slotL.pendingWebdata = data;
this.slotL.resumeLoaded();
}
}
private applyOrientation(landscape: boolean): void {
const ctx = this.hostCtx;
if (ctx === undefined) {
return;
}
const target: window.Orientation = landscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT;
window.getLastWindow(ctx).then((win: window.Window) => win.setPreferredOrientation(target))
.catch((e: BusinessError) => Logger.tag('BridgeGameContainer').w(`orientation failed: ${e.message}`));
}
/** 大厅入口 URL:params 非空用之,否则回退最小回显页(V1)。 */
private lobbySrc(): string | Resource {
return this.params.entryUrl !== '' ? this.params.entryUrl : $rawfile('test_echo.html');
}
build() {
NavDestination() {
Stack() {
// 大厅 Web(常驻)
Web({ src: this.lobbySrc(), controller: this.controllerL })
.javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
.mixedMode(MixedMode.All).cacheMode(CacheMode.None)
.geolocationAccess(true).zoomAccess(false)
.width('100%').height('100%')
.visibility(this.subgameUrl !== '' ? Visibility.Hidden : Visibility.Visible)
.onControllerAttached(() => this.setupLobby())
.onLoadIntercept((event: OnLoadInterceptEvent) =>
this.slotL !== undefined ? this.slotL.onLoadIntercept(event.data.getRequestUrl()) : false)
.onPageEnd(() => this.slotL?.onPageEnd())
.onProgressChange((event: OnProgressChangeEvent) => {
if (event.newProgress === 100) {
this.slotL?.onFirstProgress();
}
})
.onRenderExited((event: OnRenderExitedEvent) => this.slotL?.onRenderExited(event.renderExitReason))
// 子游戏 Web(临时,subgameUrl 非空时存在)
if (this.subgameUrl !== '' && this.slotS !== undefined) {
Web({ src: this.subgameUrl, controller: this.slotS.controller })
.javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
.mixedMode(MixedMode.All).cacheMode(CacheMode.None)
.geolocationAccess(true).zoomAccess(false)
.width('100%').height('100%')
.onControllerAttached(() => this.setupSubgame())
.onLoadIntercept((event: OnLoadInterceptEvent) =>
this.slotS !== undefined ? this.slotS.onLoadIntercept(event.data.getRequestUrl()) : false)
.onPageEnd(() => this.slotS?.onPageEnd())
.onProgressChange((event: OnProgressChangeEvent) => {
if (event.newProgress === 100) {
this.slotS?.onFirstProgress();
}
})
.onRenderExited((event: OnRenderExitedEvent) => this.slotS?.onRenderExited(event.renderExitReason))
}
}
.width('100%').height('100%')
}
.hideTitleBar(true)
}
}