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:
lanterngamescn
2026-06-25 18:36:59 +08:00
parent d2da223c8d
commit 2b0bd1f392
8 changed files with 254 additions and 39 deletions
+20
View File
@@ -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 { webview } from '@kit.ArkWeb';
import { common } from '@kit.AbilityKit';
import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge'; import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge';
import { AppEnv } from 'common'; import { CapabilityContext, CapabilityRegistrar } from 'feature_capabilities';
import { BridgeGameParams } from '../routes/AppRoutes'; 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)。 * 大厅/子游戏容器(对应 webviewActivity,框架 §7.1)。
* M1挂 Web + 接桥onControllerAttached 建桥 / onLoadIntercept 接桥 / onPageEnd 注桥), * 挂 Web + 接桥 + 经组装根注册全部能力 + 首次进度 100% 推 appservice/setPostUrl。
* 加载最小回显 H5 验证 JsBridge 协议与 V1yy:// 拦截)。 * 入口 URL 由 StartupOrchestrator 经 params.entryUrl 传入(file://.../index.html?Launchtype=0);
* M2:入口 URL 改为大厅 file://.../gamehall/index.htmlM3:注册全部能力 Provider * 为空时回退最小回显页(V1 验证用)
*/ */
@Component @Component
export struct BridgeGameContainer { export struct BridgeGameContainer {
@@ -16,46 +22,76 @@ export struct BridgeGameContainer {
private controller: webview.WebviewController = new webview.WebviewController(); private controller: webview.WebviewController = new webview.WebviewController();
private adapter: WebviewControllerAdapter | undefined = undefined; private adapter: WebviewControllerAdapter | undefined = undefined;
private bridge: BridgeController | 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 { aboutToAppear(): void {
// 远程调试仅 debug 包开启,release 自动关闭(CLAUDE.md 附录 B 红线) // 远程调试仅 debug 包开启,release 自动关闭(CLAUDE.md 附录 B 红线)
if (AppEnv.isDebug()) { if (AppEnv.isDebug()) {
webview.WebviewController.setWebDebuggingAccess(true); 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 { aboutToDisappear(): void {
// 释放:先清桥(回执表/注册表/启动队列),再断 UI 线程通道,最后置空防残留命中 // 释放顺序:能力 onDestroy → 清桥(回执表/注册表/启动队列)断 UI 通道 → 退订事件 → 置空
const b = this.bridge; this.registrar?.destroyAll();
if (b !== undefined) { this.bridge?.dispose();
b.dispose(); this.adapter?.dispose();
if (this.cancelForeground !== undefined) {
this.cancelForeground();
} }
const a = this.adapter; if (this.cancelBackground !== undefined) {
if (a !== undefined) { this.cancelBackground();
a.dispose();
} }
this.uploadServer.stop();
this.registrar = undefined;
this.bridge = undefined; this.bridge = undefined;
this.adapter = undefined; this.adapter = undefined;
} }
/** onControllerAttached:建桥 + 经组装根注册全部能力(框架 §A.1:能力注册放这里)。 */
private setupBridge(): void { private setupBridge(): void {
const hostCtx: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
const adapter = new WebviewControllerAdapter(this.controller); const adapter = new WebviewControllerAdapter(this.controller);
const bridge = new BridgeController(adapter); const bridge = new BridgeController(adapter);
bridge.setBridgeJs(BridgeJsLoader.load(getContext(this))); bridge.setBridgeJs(BridgeJsLoader.load(hostCtx));
// 注册最小回显所需的原生 handler(M3 替换为全部能力 Provider
bridge.registerHandler('getTime', (_data: string, cb: (resp: string) => void) => { const ctx: CapabilityContext = {
cb(Date.now().toString()); uiAbilityContext: hostCtx,
}); config: ConfigManager.load(hostCtx),
bridge.registerHandler('nativeEcho', (data: string, cb: (resp: string) => void) => { kv: KvStore.create(hostCtx),
cb(data); uploadServer: this.uploadServer,
}); log: Logger.tag('Capability'),
};
const registrar = new CapabilityRegistrar(buildCapabilities());
registrar.registerAll(bridge, ctx);
this.adapter = adapter; this.adapter = adapter;
this.bridge = bridge; 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() { build() {
NavDestination() { NavDestination() {
Web({ src: $rawfile('test_echo.html'), controller: this.controller }) Web({ src: this.webSrc(), controller: this.controller })
.javaScriptAccess(true) .javaScriptAccess(true)
.domStorageAccess(true) .domStorageAccess(true)
.fileAccess(true) .fileAccess(true)
@@ -74,9 +110,17 @@ export struct BridgeGameContainer {
return b !== undefined ? b.onLoadIntercept(url) : false; return b !== undefined ? b.onLoadIntercept(url) : false;
}) })
.onPageEnd(() => { .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; const b = this.bridge;
if (b !== undefined) { if (b !== undefined) {
b.onPageEnd(); b.callHandler(OutboundHandlers.AppService, '1');
b.callHandler(OutboundHandlers.SetPostUrl, this.uploadServer.baseUrl());
}
} }
}) })
} }
+42 -9
View File
@@ -1,29 +1,62 @@
import { common } from '@kit.AbilityKit';
import { StartupOrchestrator, StartupResult, StartupStage } from 'domain_resource';
import { RouteName, BridgeGameParams } from '../routes/AppRoutes'; import { RouteName, BridgeGameParams } from '../routes/AppRoutes';
/** /**
* 启动引导页(对应 weclomeactivity1)。 * 启动引导页(对应 weclomeactivity1,框架 §4/§8.1)。
* M0:仅路由壳,点击进入大厅容器。M2:接入 StartupOrchestrator配置/资源/app_data 注入)后再跳。 * 进入即跑 StartupOrchestrator本地配置→远程配置→资源准备→app_data 注入)
* 完成后 replace 进大厅容器;被 showmessage 阻断时弹公告。
*/ */
@Component @Component
export struct SplashPage { export struct SplashPage {
pathStack: NavPathStack = new NavPathStack(); pathStack: NavPathStack = new NavPathStack();
@State private stageText: string = '正在启动…';
@State private percent: number = 0;
@State private blocked: string = '';
private enterHall(): void { aboutToAppear(): void {
const params: BridgeGameParams = { entryUrl: '', launchType: '0' }; 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); 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() { build() {
NavDestination() { NavDestination() {
Column({ space: 16 }) { Column({ space: 16 }) {
Text('TSGame 启动引导') Text('TSGame')
.fontSize(22) .fontSize(28)
.fontWeight(FontWeight.Bold) .fontWeight(FontWeight.Bold)
Text('M0 脚手架 · 路由壳') if (this.blocked === '') {
Text(`${this.stageText} ${this.percent}%`)
.fontSize(14) .fontSize(14)
.fontColor(Color.Gray) .fontColor(Color.Gray)
Button('进入大厅') Progress({ value: this.percent, total: 100, type: ProgressType.Linear })
.onClick(() => this.enterHall()) .width('60%')
} else {
Text(this.blocked)
.fontSize(16)
.fontColor(Color.Red)
.textAlign(TextAlign.Center)
.padding({ left: 24, right: 24 })
}
} }
.width('100%') .width('100%')
.height('100%') .height('100%')
+8 -3
View File
@@ -1,3 +1,8 @@
// feature_capabilities HAR —— 能力层(导出入口 SSOT) // feature_capabilities HAR —— 能力层(导出入口 SSOT,框架 §6
// 各子模块实现完成后在此统一 export。占位常量确保 HAR 可编译。
export const feature_capabilities_MODULE_VERSION: string = '1.0.0'; // 插件框架
export { CapabilityProvider, CapabilityContext } from './src/main/ets/core/CapabilityProvider';
export { CapabilityRegistrar } from './src/main/ets/core/CapabilityRegistrar';
// 示例能力(M2 脚手架,M3 替换为真实 Provider
export { EchoSampleProvider } from './src/main/ets/providers/EchoSampleProvider';
@@ -0,0 +1,40 @@
/**
* 能力插件契约(框架 §6.1)。**桥核心对能力零感知**:能力以插件形式在容器初始化时
* 把自己的入站 handler 注册进桥,并持有 bridge 以便异步推送出站 handler。
*
* 新增/裁剪一个原生能力 = 新增/删除一个 CapabilityProvider + 在组装根 buildCapabilities() 增删一行,
* 零侵入桥核心(杜绝桥层 import 任何具体能力)。
*/
import { common } from '@kit.AbilityKit';
import { BridgeController } from 'feature_bridge';
import { ConfigManager } from 'domain_resource';
import { KvStore, LocalUploadServer } from 'platform';
import { Logger } from 'common';
/** 能力运行上下文(注入给每个 Provider)。 */
export interface CapabilityContext {
/** UIAbility 上下文(拉起系统能力/弹窗/权限)。 */
uiAbilityContext: common.UIAbilityContext;
/** 配置(取 agent/market/other 等暴露给 H5 的值)。 */
config: ConfigManager;
/** 键值存储。 */
kv: KvStore;
/** 本机上传端点(截图分享用,M3)。 */
uploadServer: LocalUploadServer;
/** 通用日志。 */
log: Logger;
}
/** 能力插件接口。 */
export interface CapabilityProvider {
/** 能力名(如 'location'/'share'),用于日志与诊断。 */
readonly name: string;
/** 把本能力的入站 handler 注册到桥;持有 bridge 以便异步推送。 */
register(bridge: BridgeController, ctx: CapabilityContext): void;
/** 应用切前台(可选,联动 §A.1)。 */
onForeground?(): void;
/** 应用切后台(可选)。 */
onBackground?(): void;
/** 容器销毁清理(可选,解绑监听/释放资源)。 */
onDestroy?(): void;
}
@@ -0,0 +1,50 @@
/**
* 能力注册器(框架 §6.2)。把一组 Provider 批量注册到桥,并统一转发生命周期。
*
* 容器持有一个 RegistraronControllerAttached 时 register(),前后台时 onForeground/onBackground()
* aboutToDisappear 时 onDestroy()。桥核心始终不感知任何具体能力。
*/
import { BridgeController } from 'feature_bridge';
import { Logger } from 'common';
import { CapabilityContext, CapabilityProvider } from './CapabilityProvider';
export class CapabilityRegistrar {
private static readonly log: Logger = Logger.tag('CapabilityRegistrar');
private readonly providers: CapabilityProvider[];
constructor(providers: CapabilityProvider[]) {
this.providers = providers;
}
/** 注册全部 Provider 的入站 handler。 */
registerAll(bridge: BridgeController, ctx: CapabilityContext): void {
for (const p of this.providers) {
p.register(bridge, ctx);
CapabilityRegistrar.log.i(`registered capability: ${p.name}`);
}
}
forEachForeground(): void {
for (const p of this.providers) {
if (p.onForeground !== undefined) {
p.onForeground();
}
}
}
forEachBackground(): void {
for (const p of this.providers) {
if (p.onBackground !== undefined) {
p.onBackground();
}
}
}
destroyAll(): void {
for (const p of this.providers) {
if (p.onDestroy !== undefined) {
p.onDestroy();
}
}
}
}
@@ -0,0 +1,23 @@
/**
* 示例能力(M2 脚手架)。验证 CapabilityProvider 插件模式可注册并被桥分发,
* 对接最小回显 H5test_echo.html 用 getTime/nativeEcho)。
*
* ⚠ M3 用真实 ProviderDeviceProvider 等)替换:buildCapabilities() 删除本行即可,
* 桥核心与容器零改动。
*/
import { BridgeController } from 'feature_bridge';
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
export class EchoSampleProvider implements CapabilityProvider {
readonly name: string = 'echo(sample)';
register(bridge: BridgeController, ctx: CapabilityContext): void {
bridge.registerHandler('getTime', (_data: string, cb: (resp: string) => void) => {
cb(Date.now().toString());
});
bridge.registerHandler('nativeEcho', (data: string, cb: (resp: string) => void) => {
cb(data);
});
ctx.log.i('echo sample registered (getTime/nativeEcho)');
}
}