- 移除 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>
220 lines
9.8 KiB
Plaintext
220 lines
9.8 KiB
Plaintext
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.js(H5 同步读),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)
|
||
}
|
||
}
|