M5(T-M5-01/02): 双 Web 大厅/子游戏架构 + WebSlot 激活协议(真机验证通过)
- 新增 entry/web/WebSlot:封装单 Web 的桥+能力+激活生命周期(setup/onLoadIntercept/onPageEnd/ onFirstProgress/activate/resumeLoaded/deactivate/onAppForeground/onAppBackground/dispose) - BridgeGameContainer 重写为 Stack 双 Web:大厅常驻(始终在 Stack,subgame 时 Hidden+onInactive)、 子游戏临时(subgameUrl 非空时渲染,onControllerAttached 各自 setup);替换 M2/M4 单 WebView loadUrl - 激活协议:进子游戏 deactivate(大厅 appservice'2'+forEachBackground+setActive(false)+onInactive)→ 注入子游戏 app_data(LT'1')→建子游戏 slot→onFirstProgress(appservice'1'+setPostUrl); 返回 dispose 子游戏→大厅 resumeLoaded(appservice'1'+getWebdata) - 每槽独立 BridgeController+buildCapabilities(结构隔离,接口不串);nav/前后台事件路由到 activeSlot - 内置测试包升级:gamehall+subA 自动驱动页(大厅→SwitchOverGameData→子游戏→backgameData→大厅) 真机验证(Pura 90 全新装):LOBBY LT=0 appservice1→SwitchOverGameData→LOBBY appservice2→ SUB LT=1 appservice1+独立 getTime→backgameData→LOBBY appservice1+getWebdata=fromSub。 大厅无二次 ready=未重载=常驻保活 ✓;接口不串 ✓;只一个激活 ✓。无 registration failed/RenderExited。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,138 +2,104 @@ import { webview } from '@kit.ArkWeb';
|
|||||||
import { common } from '@kit.AbilityKit';
|
import { common } from '@kit.AbilityKit';
|
||||||
import { window } from '@kit.ArkUI';
|
import { window } from '@kit.ArkUI';
|
||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge';
|
|
||||||
import { CapabilityContext, CapabilityRegistrar } from 'feature_capabilities';
|
|
||||||
import { ConfigManager, ResourceManager, AppDataInjector, AppDataValues } from 'domain_resource';
|
import { ConfigManager, ResourceManager, AppDataInjector, AppDataValues } from 'domain_resource';
|
||||||
import { KvStore, LocalUploadServer } from 'platform';
|
import { KvStore, LocalUploadServer } from 'platform';
|
||||||
import { OutboundHandlers } from 'contracts';
|
import { OutboundHandlers } from 'contracts';
|
||||||
import { AppEnv, EventBus, EventPayload, NavEvents, OpenGenericWebPayload, SwitchGamePayload,
|
import { AppEnv, EventBus, EventPayload, NavEvents, OpenGenericWebPayload, SwitchGamePayload,
|
||||||
BackGamePayload, Logger } from 'common';
|
BackGamePayload, Logger } from 'common';
|
||||||
import { BridgeGameParams, GenericWebParams, GenericWebResult, RouteName, AppEvents } from '../routes/AppRoutes';
|
import { BridgeGameParams, GenericWebParams, GenericWebResult, RouteName, AppEvents } from '../routes/AppRoutes';
|
||||||
import { buildCapabilities } from '../di/AppModule';
|
import { WebSlot } from '../web/WebSlot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 大厅/子游戏容器(对应 webviewActivity,框架 §7.1)。
|
* 大厅/子游戏容器(对应 webviewActivity,框架 §7.1/§7.5)。**双 Web 槽模型**:
|
||||||
* 挂 Web + 接桥 + 经组装根注册全部能力 + 首次进度 100% 推 appservice/setPostUrl。
|
* - 大厅 Web 常驻(始终在 Stack);子游戏 Web 进入创建/返回销毁(subgameUrl 空/非空驱动)。
|
||||||
* 子游戏切换(路径 A)/打开通用容器(路径 B)/返回大厅 经 NavEvents 由 NavProvider 触发、本容器执行。
|
* - 同时只有一个激活:进子游戏 → 大厅 deactivate(appservice'2'+停能力+桥关+onInactive+隐藏)、子游戏 activate;
|
||||||
|
* 返回大厅 → 销毁子游戏、大厅 resumeLoaded(appservice'1'+getWebdata)。
|
||||||
|
* - 每槽独立桥+能力(结构隔离,接口不串);cookie 全局共享、localStorage file:// 同源共享(已验证)。
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
export struct BridgeGameContainer {
|
export struct BridgeGameContainer {
|
||||||
pathStack: NavPathStack = new NavPathStack();
|
pathStack: NavPathStack = new NavPathStack();
|
||||||
params: BridgeGameParams = { entryUrl: '', launchType: '0' };
|
params: BridgeGameParams = { entryUrl: '', launchType: '0' };
|
||||||
private controller: webview.WebviewController = new webview.WebviewController();
|
private controllerL: webview.WebviewController = new webview.WebviewController();
|
||||||
private adapter: WebviewControllerAdapter | undefined = undefined;
|
private controllerS: webview.WebviewController = new webview.WebviewController();
|
||||||
private bridge: BridgeController | undefined = undefined;
|
|
||||||
private registrar: CapabilityRegistrar | undefined = undefined;
|
|
||||||
private uploadServer: LocalUploadServer = new LocalUploadServer();
|
private uploadServer: LocalUploadServer = new LocalUploadServer();
|
||||||
private hostCtx: common.UIAbilityContext | undefined = undefined;
|
private hostCtx: common.UIAbilityContext | undefined = undefined;
|
||||||
private config: ConfigManager | undefined = undefined;
|
private config: ConfigManager | undefined = undefined;
|
||||||
|
private kv: KvStore | undefined = undefined;
|
||||||
private resource: ResourceManager | undefined = undefined;
|
private resource: ResourceManager | undefined = undefined;
|
||||||
private firstProgressDone: boolean = false;
|
private slotL: WebSlot | undefined = undefined;
|
||||||
private pendingWebdata: string | undefined = undefined;
|
private slotS: WebSlot | undefined = undefined;
|
||||||
private cancels: Array<() => void> = [];
|
private cancels: Array<() => void> = [];
|
||||||
|
/** 非空 → 子游戏 Web 显示(路径 A 切换)。 */
|
||||||
|
@State private subgameUrl: string = '';
|
||||||
|
|
||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
// 远程调试仅 debug 包开启,release 自动关闭(CLAUDE.md 附录 B 红线)
|
|
||||||
if (AppEnv.isDebug()) {
|
if (AppEnv.isDebug()) {
|
||||||
webview.WebviewController.setWebDebuggingAccess(true);
|
webview.WebviewController.setWebDebuggingAccess(true);
|
||||||
}
|
}
|
||||||
// 事件订阅移到 setupBridge(桥就位后),避免冷启动事件早于桥到达被丢
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutToDisappear(): void {
|
aboutToDisappear(): void {
|
||||||
this.registrar?.destroyAll();
|
this.slotS?.dispose();
|
||||||
this.bridge?.dispose();
|
this.slotL?.dispose();
|
||||||
this.adapter?.dispose();
|
|
||||||
for (const cancel of this.cancels) {
|
for (const cancel of this.cancels) {
|
||||||
cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
this.cancels = [];
|
this.cancels = [];
|
||||||
this.uploadServer.stop();
|
this.uploadServer.stop();
|
||||||
this.registrar = undefined;
|
this.slotS = undefined;
|
||||||
this.bridge = undefined;
|
this.slotL = undefined;
|
||||||
this.adapter = undefined;
|
|
||||||
this.hostCtx = undefined;
|
this.hostCtx = undefined;
|
||||||
this.config = undefined;
|
this.config = undefined;
|
||||||
|
this.kv = undefined;
|
||||||
this.resource = undefined;
|
this.resource = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 页面进度首次 100% 时下发 appservice/setPostUrl(+子游戏返回大厅待发的 getWebdata)。幂等。
|
/** 懒初始化共享依赖(两槽共用 config/kv/resource)+ 订阅事件。 */
|
||||||
* firstProgressDone 在子游戏切换/返回大厅时复位,使重载后再次下发。 */
|
private ensureDeps(): void {
|
||||||
private firstReadyPush(): void {
|
if (this.hostCtx !== undefined) {
|
||||||
if (this.firstProgressDone) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.firstProgressDone = true;
|
|
||||||
const b = this.bridge;
|
|
||||||
if (b !== undefined) {
|
|
||||||
b.callHandler(OutboundHandlers.AppService, '1');
|
|
||||||
b.callHandler(OutboundHandlers.SetPostUrl, this.uploadServer.baseUrl());
|
|
||||||
if (this.pendingWebdata !== undefined) {
|
|
||||||
b.callHandler(OutboundHandlers.GetWebData, this.pendingWebdata);
|
|
||||||
this.pendingWebdata = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** onControllerAttached:建桥 + 经组装根注册全部能力(框架 §A.1)。 */
|
|
||||||
private setupBridge(): void {
|
|
||||||
const hostCtx: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
|
const hostCtx: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
|
||||||
this.hostCtx = hostCtx;
|
this.hostCtx = hostCtx;
|
||||||
|
this.config = ConfigManager.load(hostCtx);
|
||||||
// 🔴 V2 file:// 跨域(框架 §7.3):资源根加入"允许跨域访问"白名单(单一来源取自 ResourceManager)
|
this.kv = KvStore.create(hostCtx);
|
||||||
try {
|
this.resource = new ResourceManager(hostCtx, this.config.getLocal(), this.kv);
|
||||||
this.controller.setPathAllowingUniversalAccess([ResourceManager.resourceRoot(hostCtx)]);
|
|
||||||
} catch (e) {
|
|
||||||
Logger.tag('BridgeGameContainer').e(`setPathAllowingUniversalAccess failed: ${(e as Error).message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔴 先建桥并就位:能力注册/配置加载抛错也不致桥失效(未注册 handler 落 DefaultHandler,铁律兜底)
|
|
||||||
const adapter = new WebviewControllerAdapter(this.controller);
|
|
||||||
const bridge = new BridgeController(adapter);
|
|
||||||
this.adapter = adapter;
|
|
||||||
this.bridge = bridge;
|
|
||||||
|
|
||||||
try {
|
|
||||||
bridge.setBridgeJs(BridgeJsLoader.load(hostCtx));
|
|
||||||
const config: ConfigManager = ConfigManager.load(hostCtx);
|
|
||||||
const kv: KvStore = KvStore.create(hostCtx);
|
|
||||||
this.config = config;
|
|
||||||
this.resource = new ResourceManager(hostCtx, config.getLocal(), kv);
|
|
||||||
const ctx: CapabilityContext = {
|
|
||||||
uiAbilityContext: hostCtx,
|
|
||||||
config,
|
|
||||||
kv,
|
|
||||||
uploadServer: this.uploadServer,
|
|
||||||
log: Logger.tag('Capability'),
|
|
||||||
};
|
|
||||||
const registrar = new CapabilityRegistrar(buildCapabilities());
|
|
||||||
registrar.registerAll(bridge, ctx);
|
|
||||||
this.registrar = registrar;
|
|
||||||
} catch (e) {
|
|
||||||
Logger.tag('BridgeGameContainer').e(`capability registration failed (bridge still usable): ${(e as Error).message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subscribeEvents();
|
|
||||||
this.uploadServer.start();
|
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 {
|
private subscribeEvents(): void {
|
||||||
this.cancels.push(EventBus.on(AppEvents.FOREGROUND, () => {
|
this.cancels.push(EventBus.on(AppEvents.FOREGROUND, () => this.activeSlot()?.onAppForeground()));
|
||||||
this.registrar?.forEachForeground();
|
this.cancels.push(EventBus.on(AppEvents.BACKGROUND, () => this.activeSlot()?.onAppBackground()));
|
||||||
this.bridge?.callHandler(OutboundHandlers.AppService, '1');
|
|
||||||
}));
|
|
||||||
this.cancels.push(EventBus.on(AppEvents.BACKGROUND, () => {
|
|
||||||
this.registrar?.forEachBackground();
|
|
||||||
this.bridge?.callHandler(OutboundHandlers.AppService, '2');
|
|
||||||
}));
|
|
||||||
this.cancels.push(EventBus.on(NavEvents.OPEN_GENERIC_WEB, (p?: EventPayload) => this.openGenericWeb(p)));
|
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.SWITCH_GAME, (p?: EventPayload) => this.switchGame(p)));
|
||||||
this.cancels.push(EventBus.on(NavEvents.BACK_GAME, (p?: EventPayload) => this.backToLobby(p)));
|
this.cancels.push(EventBus.on(NavEvents.BACK_GAME, (p?: EventPayload) => this.backToLobby(p)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 路径 B:打开通用网页容器;关闭回传 → 上层 callHandler('getWebdata')。 */
|
/** 路径 B:打开通用网页容器;关闭回传 → 当前激活槽 callHandler('getWebdata')。 */
|
||||||
private openGenericWeb(p?: EventPayload): void {
|
private openGenericWeb(p?: EventPayload): void {
|
||||||
if (p === undefined) {
|
if (p === undefined) {
|
||||||
return;
|
return;
|
||||||
@@ -142,41 +108,45 @@ export struct BridgeGameContainer {
|
|||||||
const params: GenericWebParams = { url: d.url, title: d.title, data: d.data, orientation: d.orientation };
|
const params: GenericWebParams = { url: d.url, title: d.title, data: d.data, orientation: d.orientation };
|
||||||
this.pathStack.pushPathByName(RouteName.GENERIC_WEB, params, (info: PopInfo) => {
|
this.pathStack.pushPathByName(RouteName.GENERIC_WEB, params, (info: PopInfo) => {
|
||||||
const result = info.result as GenericWebResult;
|
const result = info.result as GenericWebResult;
|
||||||
this.bridge?.callHandler(OutboundHandlers.GetWebData, result !== undefined ? result.data : '');
|
this.activeSlot()?.bridge()?.callHandler(OutboundHandlers.GetWebData, result !== undefined ? result.data : '');
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 路径 A:同容器切换子游戏(loadUrl + 重注入 app_data + 方向 + 复位首屏标志)。 */
|
/** 路径 A:大厅 → 子游戏(大厅去激活并保活,创建子游戏 Web)。 */
|
||||||
private switchGame(p?: EventPayload): void {
|
private switchGame(p?: EventPayload): void {
|
||||||
if (p === undefined) {
|
if (p === undefined || this.subgameUrl !== '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = p as SwitchGamePayload;
|
const d = p as SwitchGamePayload;
|
||||||
const dir: string = d.dir;
|
|
||||||
const webtype: string = d.webtype;
|
|
||||||
const res = this.resource;
|
const res = this.resource;
|
||||||
const cfg = this.config;
|
const cfg = this.config;
|
||||||
if (dir === '' || res === undefined || cfg === undefined) {
|
if (d.dir === '' || res === undefined || cfg === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const subDir: string = `${res.paths().urlpath}/${dir}`;
|
const subDir: string = `${res.paths().urlpath}/${d.dir}`;
|
||||||
// 子游戏目录需自带 app_data.js(H5 同步读),launchtype='1'
|
// 子游戏目录自带 app_data.js(H5 同步读),launchtype='1'
|
||||||
const values: AppDataValues = AppDataInjector.buildValues(cfg.getLocal(), `${res.localVersion()}`, '1');
|
const values: AppDataValues = AppDataInjector.buildValues(cfg.getLocal(), `${res.localVersion()}`, '1');
|
||||||
AppDataInjector.inject(subDir, values);
|
AppDataInjector.inject(subDir, values);
|
||||||
this.firstProgressDone = false; // 复位:切换后重推 appservice/setPostUrl
|
this.slotL?.deactivate();
|
||||||
this.applyOrientation(webtype !== '2'); // "3" 横 / "2" 竖
|
this.slotS = new WebSlot('subgame', this.controllerS, true, this.uploadServer);
|
||||||
this.controller.loadUrl(`file://${subDir}/index.html?Launchtype=1`);
|
this.applyOrientation(d.webtype !== '2'); // "3" 横 / "2" 竖
|
||||||
|
this.subgameUrl = `file://${subDir}/index.html?Launchtype=1`; // 触发子游戏 Web 渲染 → setupSubgame
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 子游戏带数据返回大厅:重载大厅入口,加载完成后出站 getWebdata。 */
|
/** 子游戏 → 大厅(销毁子游戏 Web,大厅恢复并带回数据)。 */
|
||||||
private backToLobby(p?: EventPayload): void {
|
private backToLobby(p?: EventPayload): void {
|
||||||
if (this.params.entryUrl === '' || p === undefined) {
|
if (this.subgameUrl === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.pendingWebdata = (p as BackGamePayload).data;
|
const data: string = p !== undefined ? (p as BackGamePayload).data : '';
|
||||||
this.firstProgressDone = false;
|
this.slotS?.dispose();
|
||||||
|
this.slotS = undefined;
|
||||||
|
this.subgameUrl = ''; // 移除子游戏 Web
|
||||||
this.applyOrientation(true); // 大厅横屏
|
this.applyOrientation(true); // 大厅横屏
|
||||||
this.controller.loadUrl(this.params.entryUrl);
|
if (this.slotL !== undefined) {
|
||||||
|
this.slotL.pendingWebdata = data;
|
||||||
|
this.slotL.resumeLoaded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyOrientation(landscape: boolean): void {
|
private applyOrientation(landscape: boolean): void {
|
||||||
@@ -189,42 +159,50 @@ export struct BridgeGameContainer {
|
|||||||
.catch((e: BusinessError) => Logger.tag('BridgeGameContainer').w(`orientation failed: ${e.message}`));
|
.catch((e: BusinessError) => Logger.tag('BridgeGameContainer').w(`orientation failed: ${e.message}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 入口 URL:params 非空用之,否则回退最小回显页(V1)。 */
|
/** 大厅入口 URL:params 非空用之,否则回退最小回显页(V1)。 */
|
||||||
private webSrc(): string | Resource {
|
private lobbySrc(): string | Resource {
|
||||||
return this.params.entryUrl !== '' ? this.params.entryUrl : $rawfile('test_echo.html');
|
return this.params.entryUrl !== '' ? this.params.entryUrl : $rawfile('test_echo.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
NavDestination() {
|
NavDestination() {
|
||||||
Web({ src: this.webSrc(), controller: this.controller })
|
Stack() {
|
||||||
.javaScriptAccess(true)
|
// 大厅 Web(常驻)
|
||||||
.domStorageAccess(true)
|
Web({ src: this.lobbySrc(), controller: this.controllerL })
|
||||||
.fileAccess(true)
|
.javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
|
||||||
.mixedMode(MixedMode.All)
|
.mixedMode(MixedMode.All).cacheMode(CacheMode.None)
|
||||||
.cacheMode(CacheMode.None)
|
.geolocationAccess(true).zoomAccess(false)
|
||||||
.geolocationAccess(true)
|
.width('100%').height('100%')
|
||||||
.zoomAccess(false)
|
.visibility(this.subgameUrl !== '' ? Visibility.Hidden : Visibility.Visible)
|
||||||
.width('100%')
|
.onControllerAttached(() => this.setupLobby())
|
||||||
.height('100%')
|
.onLoadIntercept((event: OnLoadInterceptEvent) =>
|
||||||
.onControllerAttached(() => {
|
this.slotL !== undefined ? this.slotL.onLoadIntercept(event.data.getRequestUrl()) : false)
|
||||||
this.setupBridge();
|
.onPageEnd(() => this.slotL?.onPageEnd())
|
||||||
})
|
|
||||||
.onLoadIntercept((event: OnLoadInterceptEvent) => {
|
|
||||||
const url: string = event.data.getRequestUrl();
|
|
||||||
const b = this.bridge;
|
|
||||||
return b !== undefined ? b.onLoadIntercept(url) : false;
|
|
||||||
})
|
|
||||||
.onPageEnd(() => {
|
|
||||||
this.bridge?.onPageEnd();
|
|
||||||
})
|
|
||||||
.onProgressChange((event: OnProgressChangeEvent) => {
|
.onProgressChange((event: OnProgressChangeEvent) => {
|
||||||
// 首次/每次切换后页面到 100% → 推 appservice(前台)+setPostUrl(对齐 Android onProgressChanged==100)。
|
|
||||||
// 用 100%(而非更早的 onPageEnd):确保 H5 在 WebViewJavascriptBridgeReady 里已注册 appservice/setPostUrl
|
|
||||||
// 等 handler,避免出站早于注册被丢。loadUrl 切子游戏/返回大厅会再次产生 100% 回调(已复位 firstProgressDone)。
|
|
||||||
if (event.newProgress === 100) {
|
if (event.newProgress === 100) {
|
||||||
this.firstReadyPush();
|
this.slotL?.onFirstProgress();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 子游戏 Web(临时,subgameUrl 非空时存在)
|
||||||
|
if (this.subgameUrl !== '') {
|
||||||
|
Web({ src: this.subgameUrl, controller: this.controllerS })
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%').height('100%')
|
||||||
}
|
}
|
||||||
.hideTitleBar(true)
|
.hideTitleBar(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { webview } from '@kit.ArkWeb';
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
|
import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge';
|
||||||
|
import { CapabilityContext, CapabilityRegistrar } from 'feature_capabilities';
|
||||||
|
import { ConfigManager } from 'domain_resource';
|
||||||
|
import { KvStore, LocalUploadServer } from 'platform';
|
||||||
|
import { OutboundHandlers } from 'contracts';
|
||||||
|
import { Logger } from 'common';
|
||||||
|
import { buildCapabilities } from '../di/AppModule';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个 Web 槽(M5 双 Web 架构,框架 §7.5)。封装一个 Web 的桥 + 能力 + 激活生命周期。
|
||||||
|
* 大厅槽常驻;子游戏槽进入创建/返回销毁。每槽独立 BridgeController + 能力(结构隔离,接口不串)。
|
||||||
|
*
|
||||||
|
* controller 由容器持有并绑定到对应 Web 组件;本类在 onControllerAttached 后 setup()。
|
||||||
|
*/
|
||||||
|
export class WebSlot {
|
||||||
|
private static readonly log: Logger = Logger.tag('WebSlot');
|
||||||
|
readonly role: string;
|
||||||
|
readonly controller: webview.WebviewController;
|
||||||
|
/** 子游戏返回大厅时待下发给大厅的数据(getWebdata)。 */
|
||||||
|
pendingWebdata: string | undefined = undefined;
|
||||||
|
|
||||||
|
private readonly uploadServer: LocalUploadServer;
|
||||||
|
private adapter: WebviewControllerAdapter | undefined = undefined;
|
||||||
|
private bridgeCtrl: BridgeController | undefined = undefined;
|
||||||
|
private registrar: CapabilityRegistrar | undefined = undefined;
|
||||||
|
private firstDone: boolean = false;
|
||||||
|
private active: boolean;
|
||||||
|
|
||||||
|
constructor(role: string, controller: webview.WebviewController, active: boolean, uploadServer: LocalUploadServer) {
|
||||||
|
this.role = role;
|
||||||
|
this.controller = controller;
|
||||||
|
this.active = active;
|
||||||
|
this.uploadServer = uploadServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge(): BridgeController | undefined {
|
||||||
|
return this.bridgeCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(): boolean {
|
||||||
|
return this.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** onControllerAttached:建桥 + 经组装根注册能力(每槽独立一套)。 */
|
||||||
|
setup(hostCtx: common.UIAbilityContext, config: ConfigManager, kv: KvStore, resourceRoot: string): void {
|
||||||
|
try {
|
||||||
|
this.controller.setPathAllowingUniversalAccess([resourceRoot]);
|
||||||
|
} catch (e) {
|
||||||
|
WebSlot.log.e(`[${this.role}] setPathAllowingUniversalAccess failed: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
const adapter = new WebviewControllerAdapter(this.controller);
|
||||||
|
const bridge = new BridgeController(adapter);
|
||||||
|
this.adapter = adapter;
|
||||||
|
this.bridgeCtrl = bridge;
|
||||||
|
bridge.setActive(this.active);
|
||||||
|
try {
|
||||||
|
bridge.setBridgeJs(BridgeJsLoader.load(hostCtx));
|
||||||
|
const ctx: CapabilityContext = {
|
||||||
|
uiAbilityContext: hostCtx,
|
||||||
|
config,
|
||||||
|
kv,
|
||||||
|
uploadServer: this.uploadServer,
|
||||||
|
log: Logger.tag(`Cap-${this.role}`),
|
||||||
|
};
|
||||||
|
const registrar = new CapabilityRegistrar(buildCapabilities());
|
||||||
|
registrar.registerAll(bridge, ctx);
|
||||||
|
this.registrar = registrar;
|
||||||
|
} catch (e) {
|
||||||
|
WebSlot.log.e(`[${this.role}] capability registration failed (bridge usable): ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoadIntercept(url: string): boolean {
|
||||||
|
const b = this.bridgeCtrl;
|
||||||
|
return b !== undefined ? b.onLoadIntercept(url) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageEnd(): void {
|
||||||
|
this.bridgeCtrl?.onPageEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 页面首次 100%:下发 appservice('1') + setPostUrl(+ 返回大厅待发 getWebdata)。 */
|
||||||
|
onFirstProgress(): void {
|
||||||
|
if (this.firstDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.firstDone = true;
|
||||||
|
const b = this.bridgeCtrl;
|
||||||
|
if (b === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
b.callHandler(OutboundHandlers.AppService, '1');
|
||||||
|
b.callHandler(OutboundHandlers.SetPostUrl, this.uploadServer.baseUrl());
|
||||||
|
this.flushPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 激活(onActive + 桥放行 + 能力前台)。页面已加载的大厅返回时另调 resumeLoaded 补发 appservice。 */
|
||||||
|
activate(): void {
|
||||||
|
this.active = true;
|
||||||
|
try {
|
||||||
|
this.controller.onActive();
|
||||||
|
} catch (e) {
|
||||||
|
WebSlot.log.w(`[${this.role}] onActive failed: ${(e as BusinessError).message}`);
|
||||||
|
}
|
||||||
|
this.bridgeCtrl?.setActive(true);
|
||||||
|
this.registrar?.forEachForeground();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 大厅从子游戏返回(页面仍在、无新 100%):激活并直接补发 appservice('1') + 待发 getWebdata。 */
|
||||||
|
resumeLoaded(): void {
|
||||||
|
this.activate();
|
||||||
|
this.bridgeCtrl?.callHandler(OutboundHandlers.AppService, '1');
|
||||||
|
this.flushPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 去激活(先发 appservice('2'),再停能力、关桥、onInactive)。 */
|
||||||
|
deactivate(): void {
|
||||||
|
this.bridgeCtrl?.callHandler(OutboundHandlers.AppService, '2'); // 彼时仍激活,可下发
|
||||||
|
this.registrar?.forEachBackground();
|
||||||
|
this.bridgeCtrl?.setActive(false);
|
||||||
|
this.active = false;
|
||||||
|
try {
|
||||||
|
this.controller.onInactive();
|
||||||
|
} catch (e) {
|
||||||
|
WebSlot.log.w(`[${this.role}] onInactive failed: ${(e as BusinessError).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** App 前后台 → 当前激活槽下发 appservice。 */
|
||||||
|
onAppForeground(): void {
|
||||||
|
this.registrar?.forEachForeground();
|
||||||
|
this.bridgeCtrl?.callHandler(OutboundHandlers.AppService, '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
onAppBackground(): void {
|
||||||
|
this.registrar?.forEachBackground();
|
||||||
|
this.bridgeCtrl?.callHandler(OutboundHandlers.AppService, '2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 销毁(子游戏返回大厅时)。 */
|
||||||
|
dispose(): void {
|
||||||
|
this.registrar?.destroyAll();
|
||||||
|
this.bridgeCtrl?.dispose();
|
||||||
|
this.adapter?.dispose();
|
||||||
|
this.registrar = undefined;
|
||||||
|
this.bridgeCtrl = undefined;
|
||||||
|
this.adapter = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private flushPending(): void {
|
||||||
|
if (this.pendingWebdata !== undefined) {
|
||||||
|
this.bridgeCtrl?.callHandler(OutboundHandlers.GetWebData, this.pendingWebdata);
|
||||||
|
this.pendingWebdata = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user