M2(容器+能力骨架+启动接线): CapabilityProvider/Registrar/AppModule + BridgeGameContainer 完整化 + Splash 接编排
T-M2-13 能力插件框架骨架:CapabilityProvider/CapabilityContext 接口、CapabilityRegistrar(批量注册+生命周期转发)、
组装根 AppModule.buildCapabilities()(唯一知道全部能力处)、EchoSampleProvider 样板
T-M2-10 BridgeGameContainer 完整化:经 Registrar 注册能力(替换内联 handler)、首次进度 100% 推 appservice/setPostUrl、
加载 params.entryUrl(回退 test_echo)、前后台 EventBus 联动、aboutToDisappear 全量清理;getContext→getHostContext 去弃用
SplashPage:进入即跑 StartupOrchestrator,完成 replace 进大厅、showmessage 阻断弹公告、进度条
内置测试大厅 rawfile/gamehall_builtin.zip(gamehall/index.html+data/test.json+version.xml,正斜杠标准 zip),
作 M2 端到端 + V2(file:// XHR) 离线验证页;真实大厅由远程 zip 在生产下发
devecocli build 通过。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 组装根(框架 §6.2 DI)。**唯一知道"全部能力"的地方**。
|
||||
*
|
||||
* 桥核心、容器都不感知具体能力;新增/裁剪能力只改这里一行。
|
||||
* M2:仅示例能力(验证插件模式)。M3:替换为全部真实 Provider + 占位桩
|
||||
* (RoomStubProvider/PayStubProvider,§6.5),契约 handler 名/数量不变。
|
||||
*/
|
||||
import { CapabilityProvider, EchoSampleProvider } from 'feature_capabilities';
|
||||
|
||||
export function buildCapabilities(): CapabilityProvider[] {
|
||||
return [
|
||||
new EchoSampleProvider(),
|
||||
// —— M3 在此装配(顺序无关,互不依赖)——
|
||||
// new ShareProvider(), new LoginProvider(), new LocationProvider(), new AudioProvider(),
|
||||
// new ShakeProvider(), new DeviceProvider(), new ClipboardProvider(), new NetworkProvider(),
|
||||
// new VibrateProvider(), new ScanProvider(), new CameraProvider(), new PhotoProvider(),
|
||||
// new NavProvider(), new AppSystemProvider(),
|
||||
// new RoomStubProvider(), new PayStubProvider(), // 暂缓能力占位桩(§6.5)
|
||||
];
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
import { webview } from '@kit.ArkWeb';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge';
|
||||
import { AppEnv } from 'common';
|
||||
import { BridgeGameParams } from '../routes/AppRoutes';
|
||||
import { CapabilityContext, CapabilityRegistrar } from 'feature_capabilities';
|
||||
import { ConfigManager } from 'domain_resource';
|
||||
import { KvStore, LocalUploadServer } from 'platform';
|
||||
import { OutboundHandlers } from 'contracts';
|
||||
import { AppEnv, EventBus, Logger } from 'common';
|
||||
import { BridgeGameParams, AppEvents } from '../routes/AppRoutes';
|
||||
import { buildCapabilities } from '../di/AppModule';
|
||||
|
||||
/**
|
||||
* 大厅/子游戏容器(对应 webviewActivity,框架 §7.1)。
|
||||
* M1:挂 Web + 接桥(onControllerAttached 建桥 / onLoadIntercept 接桥 / onPageEnd 注桥),
|
||||
* 加载最小回显 H5 验证 JsBridge 协议与 V1(yy:// 拦截)。
|
||||
* M2:入口 URL 改为大厅 file://.../gamehall/index.html;M3:注册全部能力 Provider。
|
||||
* 挂 Web + 接桥 + 经组装根注册全部能力 + 首次进度 100% 推 appservice/setPostUrl。
|
||||
* 入口 URL 由 StartupOrchestrator 经 params.entryUrl 传入(file://.../index.html?Launchtype=0);
|
||||
* 为空时回退最小回显页(V1 验证用)。
|
||||
*/
|
||||
@Component
|
||||
export struct BridgeGameContainer {
|
||||
@@ -16,46 +22,76 @@ export struct BridgeGameContainer {
|
||||
private controller: webview.WebviewController = new webview.WebviewController();
|
||||
private adapter: WebviewControllerAdapter | undefined = undefined;
|
||||
private bridge: BridgeController | undefined = undefined;
|
||||
private registrar: CapabilityRegistrar | undefined = undefined;
|
||||
private uploadServer: LocalUploadServer = new LocalUploadServer();
|
||||
private firstProgressDone: boolean = false;
|
||||
private cancelForeground: (() => void) | undefined = undefined;
|
||||
private cancelBackground: (() => void) | undefined = undefined;
|
||||
|
||||
aboutToAppear(): void {
|
||||
// 远程调试仅 debug 包开启,release 自动关闭(CLAUDE.md 附录 B 红线)
|
||||
if (AppEnv.isDebug()) {
|
||||
webview.WebviewController.setWebDebuggingAccess(true);
|
||||
}
|
||||
// 前后台联动:EntryAbility 经 EventBus 广播 → 转给各能力 + callHandler('appservice')
|
||||
this.cancelForeground = EventBus.on(AppEvents.FOREGROUND, () => {
|
||||
this.registrar?.forEachForeground();
|
||||
this.bridge?.callHandler(OutboundHandlers.AppService, '1');
|
||||
});
|
||||
this.cancelBackground = EventBus.on(AppEvents.BACKGROUND, () => {
|
||||
this.registrar?.forEachBackground();
|
||||
this.bridge?.callHandler(OutboundHandlers.AppService, '2');
|
||||
});
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
// 释放:先清桥(回执表/注册表/启动队列),再断 UI 线程通道,最后置空防残留命中
|
||||
const b = this.bridge;
|
||||
if (b !== undefined) {
|
||||
b.dispose();
|
||||
// 释放顺序:能力 onDestroy → 清桥(回执表/注册表/启动队列)→ 断 UI 通道 → 退订事件 → 置空
|
||||
this.registrar?.destroyAll();
|
||||
this.bridge?.dispose();
|
||||
this.adapter?.dispose();
|
||||
if (this.cancelForeground !== undefined) {
|
||||
this.cancelForeground();
|
||||
}
|
||||
const a = this.adapter;
|
||||
if (a !== undefined) {
|
||||
a.dispose();
|
||||
if (this.cancelBackground !== undefined) {
|
||||
this.cancelBackground();
|
||||
}
|
||||
this.uploadServer.stop();
|
||||
this.registrar = undefined;
|
||||
this.bridge = undefined;
|
||||
this.adapter = undefined;
|
||||
}
|
||||
|
||||
/** onControllerAttached:建桥 + 经组装根注册全部能力(框架 §A.1:能力注册放这里)。 */
|
||||
private setupBridge(): void {
|
||||
const hostCtx: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
|
||||
const adapter = new WebviewControllerAdapter(this.controller);
|
||||
const bridge = new BridgeController(adapter);
|
||||
bridge.setBridgeJs(BridgeJsLoader.load(getContext(this)));
|
||||
// 注册最小回显所需的原生 handler(M3 替换为全部能力 Provider)
|
||||
bridge.registerHandler('getTime', (_data: string, cb: (resp: string) => void) => {
|
||||
cb(Date.now().toString());
|
||||
});
|
||||
bridge.registerHandler('nativeEcho', (data: string, cb: (resp: string) => void) => {
|
||||
cb(data);
|
||||
});
|
||||
bridge.setBridgeJs(BridgeJsLoader.load(hostCtx));
|
||||
|
||||
const ctx: CapabilityContext = {
|
||||
uiAbilityContext: hostCtx,
|
||||
config: ConfigManager.load(hostCtx),
|
||||
kv: KvStore.create(hostCtx),
|
||||
uploadServer: this.uploadServer,
|
||||
log: Logger.tag('Capability'),
|
||||
};
|
||||
const registrar = new CapabilityRegistrar(buildCapabilities());
|
||||
registrar.registerAll(bridge, ctx);
|
||||
|
||||
this.adapter = adapter;
|
||||
this.bridge = bridge;
|
||||
this.registrar = registrar;
|
||||
this.uploadServer.start();
|
||||
}
|
||||
|
||||
/** 入口 URL:params 非空用之,否则回退最小回显页(V1)。 */
|
||||
private webSrc(): string | Resource {
|
||||
return this.params.entryUrl !== '' ? this.params.entryUrl : $rawfile('test_echo.html');
|
||||
}
|
||||
|
||||
build() {
|
||||
NavDestination() {
|
||||
Web({ src: $rawfile('test_echo.html'), controller: this.controller })
|
||||
Web({ src: this.webSrc(), controller: this.controller })
|
||||
.javaScriptAccess(true)
|
||||
.domStorageAccess(true)
|
||||
.fileAccess(true)
|
||||
@@ -74,9 +110,17 @@ export struct BridgeGameContainer {
|
||||
return b !== undefined ? b.onLoadIntercept(url) : false;
|
||||
})
|
||||
.onPageEnd(() => {
|
||||
const b = this.bridge;
|
||||
if (b !== undefined) {
|
||||
b.onPageEnd();
|
||||
this.bridge?.onPageEnd();
|
||||
})
|
||||
.onProgressChange((event: OnProgressChangeEvent) => {
|
||||
// 首次进度 100% → 推 appservice(前台) + setPostUrl(与 Android onProgressChanged==100 一致)
|
||||
if (event.newProgress === 100 && !this.firstProgressDone) {
|
||||
this.firstProgressDone = true;
|
||||
const b = this.bridge;
|
||||
if (b !== undefined) {
|
||||
b.callHandler(OutboundHandlers.AppService, '1');
|
||||
b.callHandler(OutboundHandlers.SetPostUrl, this.uploadServer.baseUrl());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,29 +1,62 @@
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { StartupOrchestrator, StartupResult, StartupStage } from 'domain_resource';
|
||||
import { RouteName, BridgeGameParams } from '../routes/AppRoutes';
|
||||
|
||||
/**
|
||||
* 启动引导页壳(对应 weclomeactivity1)。
|
||||
* M0:仅路由壳,点击进入大厅容器。M2:接入 StartupOrchestrator(配置/资源/app_data 注入)后再跳。
|
||||
* 启动引导页(对应 weclomeactivity1,框架 §4/§8.1)。
|
||||
* 进入即跑 StartupOrchestrator(本地配置→远程配置→资源准备→app_data 注入),
|
||||
* 完成后 replace 进大厅容器;被 showmessage 阻断时弹公告。
|
||||
*/
|
||||
@Component
|
||||
export struct SplashPage {
|
||||
pathStack: NavPathStack = new NavPathStack();
|
||||
@State private stageText: string = '正在启动…';
|
||||
@State private percent: number = 0;
|
||||
@State private blocked: string = '';
|
||||
|
||||
private enterHall(): void {
|
||||
const params: BridgeGameParams = { entryUrl: '', launchType: '0' };
|
||||
this.pathStack.replacePathByName(RouteName.BRIDGE_GAME, params, false);
|
||||
aboutToAppear(): void {
|
||||
this.runStartup();
|
||||
}
|
||||
|
||||
private async runStartup(): Promise<void> {
|
||||
const ctx: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
|
||||
const orchestrator = new StartupOrchestrator(ctx, (stage: StartupStage, percent: number) => {
|
||||
this.stageText = stage;
|
||||
this.percent = percent;
|
||||
});
|
||||
try {
|
||||
const result: StartupResult = await orchestrator.run();
|
||||
if (result.enter) {
|
||||
const params: BridgeGameParams = { entryUrl: result.entryUrl, launchType: '0' };
|
||||
this.pathStack.replacePathByName(RouteName.BRIDGE_GAME, params, false);
|
||||
} else {
|
||||
this.blocked = result.message;
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
this.blocked = `启动失败:${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
NavDestination() {
|
||||
Column({ space: 16 }) {
|
||||
Text('TSGame 启动引导')
|
||||
.fontSize(22)
|
||||
Text('TSGame')
|
||||
.fontSize(28)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
Text('M0 脚手架 · 路由壳')
|
||||
.fontSize(14)
|
||||
.fontColor(Color.Gray)
|
||||
Button('进入大厅')
|
||||
.onClick(() => this.enterHall())
|
||||
if (this.blocked === '') {
|
||||
Text(`${this.stageText} ${this.percent}%`)
|
||||
.fontSize(14)
|
||||
.fontColor(Color.Gray)
|
||||
Progress({ value: this.percent, total: 100, type: ProgressType.Linear })
|
||||
.width('60%')
|
||||
} else {
|
||||
Text(this.blocked)
|
||||
.fontSize(16)
|
||||
.fontColor(Color.Red)
|
||||
.textAlign(TextAlign.Center)
|
||||
.padding({ left: 24, right: 24 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user