Files
youle_app_ohos/docs/设计文档/TSGame_HarmonyOS框架设计与开发指南.md
T
lanterngamescnandClaude Opus 4.8 0fcee6de1e 文档同步:远程配置格式按线上真实格式纠正(契约§4.3/4.4 + 框架指南 + WBS)
代码已对齐线上真实远程配置格式,同步修订设计文档(此前 §4 误录为废弃的
Bean1/GameupdateUtil 格式,会误导后续开发):
- 契约§4.3 配置 JSON 改为真实格式:顶层仅 agentlist、层级 agent→game→channel→market、
  资源字段 game_zip、marketid/版本为数字、无二级 url 二次请求;附旧格式纠正说明。
- §4.4 分层匹配改为真实遍历与累积规则 + gameid 回退 version.xml。
- §4.2 gameconfig 改生产端点;§5.4/启动流程图 game_download→game_zip;验收清单同步。
- 框架设计指南 §8.2/§8.3/落地映射表、Plan WBS T-M2-05/06 描述同步。

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

822 lines
60 KiB
Markdown
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.
# TSGame HarmonyOS 应用框架设计与开发指南
> **配套文档**:本设计严格落地《[TSGame_原生与H5接口契约总规范](./TSGame_原生与H5接口契约总规范.md)》(下称《契约规范》)。**契约规范定义"对外必须一致的行为",本文定义"HarmonyOS 侧如何优雅、高效地实现它"。** 二者配合即可让现有 H5 **零改动**运行。
>
> **设计目标**:现代化(ArkTS / ArkUI 声明式 / Stage 模型)、高性能(Web 保活、并发解压下载、零主线程阻塞)、架构优雅(分层 + 依赖倒置 + 能力插件化)、专业成熟(清晰边界、可测试、可观测、可演进)。
>
> **目标平台**HarmonyOS NEXTAPI 12+,建议 API 17/23),ArkWeb(方舟 Web)组件。
---
## 目录
1. [设计原则](#1-设计原则)
2. [Android → HarmonyOS 能力映射总表](#2-android--harmonyos-能力映射总表)
3. [总体架构(分层 + 模块)](#3-总体架构分层--模块)
4. [工程结构(模块化拆分)](#4-工程结构模块化拆分)
5. [核心一:JsBridge 桥引擎设计](#5-核心一jsbridge-桥引擎设计)
6. [核心二:能力插件框架(CapabilityProvider](#6-核心二能力插件框架capabilityprovider)
7. [容器设计:双容器模型](#7-容器设计双容器模型)
8. [启动编排与配置/资源子系统](#8-启动编排与配置资源子系统)
9. [性能架构](#9-性能架构)
10. [横切关注点](#10-横切关注点)
11. [契约可追溯性矩阵](#11-契约可追溯性矩阵)
12. [实施路线图(里程碑)](#12-实施路线图里程碑)
13. [验收标准](#13-验收标准)
- [附录 AArkTS / ArkUI 实现适配清单](#附录-aarkts--arkui-实现适配清单)
- [附录 B:生产加固与合规清单](#附录-b生产加固与合规清单)
---
## 1. 设计原则
| 原则 | 说明 | 在本框架的体现 |
|---|---|---|
| **契约优先(Contract-First** | H5 看到的协议是不可变契约,原生实现围绕契约展开 | 桥协议、handler 名、数据结构以《契约规范》为单一事实来源(SSOT),用 TS 类型固化 |
| **依赖倒置(DIP** | 上层依赖抽象,不依赖具体平台实现 | 桥引擎依赖 `ICapability`/`IPlatformService` 接口,能力模块按需注入 |
| **能力插件化(Plugin** | 每个原生能力是一个自注册插件 | `CapabilityProvider` 注册表,新增能力 = 新增一个 Provider,零侵入桥核心 |
| **单一职责 + 分层** | 桥、能力、配置、资源、平台服务各司其职 | 见 §3 分层 |
| **异步非阻塞** | IO/CPU 重活不上 UI 线程 | TaskPool/Worker 跑下载、解压、加解密;UI 线程只做编排与渲染 |
| **可观测(Observability** | 全链路可日志、可埋点、可诊断 | 统一 `Logger` + `BridgeTracer`,每条桥消息可追踪 |
| **可演进** | 旧契约保留,新能力可叠加 | 契约层与实现层解耦;补齐项(QQ/抖音回调等)以新增 Provider 落地 |
---
## 2. Android → HarmonyOS 能力映射总表
> 这是把《契约规范》的 Android 机制翻译到 HarmonyOS 的"罗塞塔石碑",框架的所有实现都基于此映射。
| 契约机制(Android | HarmonyOS ArkWeb 等价物 | 说明 |
|---|---|---|
| `WebView`X5/BridgeWebView | `Web({ src, controller })` + `webview.WebviewController` | 声明式组件,Controller 控制行为 |
| `shouldOverrideUrlLoading` 拦截 `yy://` | **`onLoadIntercept`**(返回 `true` 拦截) | 桥协议核心;`onLoadIntercept` 在 loadUrl 与 iframe 加载时均触发,正好匹配 H5 用 iframe.src 发起 `yy://` |
| `loadUrl("javascript:...")` / `evaluateJavascript` | **`controller.runJavaScript(script)`**(带 Promise 回调) | 原生→H5 下发 |
| `onPageFinished` 注入桥 JS | **`onPageEnd`** 中 `runJavaScript(桥JS)` | 注入 `WebViewJavascriptBridge.js` |
| `addJavascriptInterface(obj, name)`@JavascriptInterface | **`javaScriptProxy` 属性** 或 `controller.registerJavaScriptProxy()` | 仅通用网页容器 `settings` 用 |
| `onProgressChanged` | **`onProgressChange`** | 进度=100% 触发 `appservice`/`setPostUrl` |
| `onResume/onPause/onStop`(前后台) | `UIAbility.onForeground/onBackground` + 页面 `onPageShow/onPageHide` | 推送 `appservice` |
| WebSettings.setJavaScriptEnabled | `.javaScriptAccess(true)` | |
| setDomStorageEnabled | `.domStorageAccess(true)` | |
| setAllowFileAccess | `.fileAccess(true)`(默认开) | 访问本地文件系统 |
| setAllowUniversalAccessFromFileURLsfile:// 跨域) | **无直接等价属性**;用官方跨域方案:`onInterceptRequest`/`WebSchemeHandler` 自定义协议接管本地资源,或以自定义 scheme 加载首页 | 见 §7.3;这是 file:// 加载下 H5 发 XHR/fetch 取本地资源的关键,**M2 必须验证** |
| setMixedContentMode(ALWAYS_ALLOW) | `.mixedMode(MixedMode.All)` | |
| setCacheMode(LOAD_NO_CACHE) | `.cacheMode(CacheMode.None)` | 主容器禁缓存 |
| setGeolocationEnabled | `.geolocationAccess(true)` | |
| setSupportZoom(false) | `.zoomAccess(false)` | |
| setWebContentsDebuggingEnabled | `webview.WebviewController.setWebDebuggingAccess(true)` | 在 `aboutToAppear` 设置 |
| `startActivityForResult`/`setResult`(容器间回传) | 路由参数 + 回调 / `emitter` 事件 / `AppStorage` | 通用网页容器回传 `data`(结果码 101 语义)见 §7.2 |
| Intent extra | `router`/`Navigation` 参数对象 | |
| SharedPreferences | `@ohos.data.preferences`KV | `urlpath`/`upurlpath` 等 |
| 本机 HTTP server(截图上传) | **`@ohos.net.http` 是客户端、无内置服务端**;用 `@ohos.net.socket`(TCP) 自建轻量 HTTP 服务,或 NAPI 原生 server(如 cpp-httplib | 监听本机端口,地址经 `setPostUrl` 告知 H5 |
| OkHttp | `@ohos.net.http` / `rcp`Remote Communication Kit | 远程配置、下载 |
| zip 解压 | `@ohos.zlib``decompressFile` | TaskPool 内执行 |
| 友盟/Bugly | HUAWEI Analytics Kit / APM / Crash Service | 可观测,非契约 |
| 微信/QQ/抖音/高德 SDK | 各厂商 HarmonyOS SDK | 能力 Provider 内对接 |
---
## 3. 总体架构(分层 + 模块)
采用**自上而下五层 + 横切层**,依赖方向单向向下,跨层只依赖接口。
```
┌────────────────────────────────────────────────────────────────────────┐
│ ① 应用/编排层 App & Orchestration │
│ EntryAbility · StartupOrchestrator · 路由(Navigation) · 全局DI容器 │
├────────────────────────────────────────────────────────────────────────┤
│ ② 容器层 Containers (ArkUI Pages) │
│ BridgeGameContainer(大厅/子游戏) │ GenericWebContainer(通用网页) │
├────────────────────────────────────────────────────────────────────────┤
│ ③ 桥引擎层 Bridge Engine ④ 能力层 Capabilities (插件) │
│ BridgeController · MessageCodec │ Share/Login/Pay/Location/Audio/ │
│ HandlerRegistry · QueueFlusher │ Shake/Device/Clipboard/Net/... │
│ SettingsProxy(通用容器) │ 每个 = 一个 CapabilityProvider │
├────────────────────────────────────────────────────────────────────────┤
│ ⑤ 领域服务层 Domain Services │
│ ConfigManager · ResourceManager · AppDataInjector · VersionResolver │
├────────────────────────────────────────────────────────────────────────┤
│ ⑥ 平台服务层 Platform Services (对 SDK 的薄封装) │
│ HttpClient · Downloader · Unzipper · KvStore · PermissionGuard · │
│ LocalUploadServer · FileSystem · TaskScheduler(TaskPool/Worker) │
└────────────────────────────────────────────────────────────────────────┘
横切层 Cross-cutting Logger/Tracer · ErrorCenter · EventBus · DIContainer · Contracts(TS类型)
```
**关键约束**
- 桥引擎层(③)**不认识任何具体能力**,只持有 `HandlerRegistry`;能力层(④)启动时把自己的 handler 注册进去 → **桥核心对能力数量零感知**
- 能力层(④)通过平台服务层(⑥)使用系统/厂商 SDK,**绝不**直接被容器层调用(保持单向)。
- `Contracts` 横切层用 TypeScript `interface`/`enum`/常量固化《契约规范》的 handler 名与数据结构,**全工程唯一来源**。
---
## 4. 工程结构(模块化拆分)
按 HarmonyOS **HAR/HSP 多模块**组织,强边界、可独立编译、可并行开发:
```
TSGameHarmony/
├── entry/ # 入口 HAP(应用/编排层 ①②)
│ └── src/main/ets/
│ ├── entryability/EntryAbility.ets
│ ├── startup/StartupOrchestrator.ets
│ ├── pages/
│ │ ├── SplashPage.ets # 启动引导(对应 weclomeactivity1
│ │ ├── BridgeGameContainer.ets # 大厅/子游戏容器
│ │ └── GenericWebContainer.ets # 通用网页容器(对应 openwebActivity1)
│ └── di/AppModule.ets # 组装根:把各 Provider 注入桥
├── feature_bridge/ (HAR) # 桥引擎层 ③ —— 与业务解耦的可复用桥
│ └── ets/
│ ├── BridgeController.ets
│ ├── MessageCodec.ets · Message.ets
│ ├── HandlerRegistry.ets
│ ├── SettingsProxy.ets # 通用容器 @javaScriptProxy 对象
│ └── assets/WebViewJavascriptBridge.js # 直接复用 lzyzsd 原文件
├── feature_capabilities/ (HAR) # 能力层 ④ —— 每个能力一个文件夹
│ └── ets/
│ ├── CapabilityProvider.ets # 插件接口 + 注册表
│ ├── share/ login/ pay/ location/ audio/ shake/
│ ├── device/ clipboard/ network/ vibrate/ scan/ camera/ photo/ room/
│ └── index.ets # 汇总导出 provideAll()
├── domain_resource/ (HAR) # 领域服务层 ⑤
│ └── ets/ ConfigManager · ResourceManager · AppDataInjector · VersionResolver
├── platform/ (HAR) # 平台服务层 ⑥
│ └── ets/ HttpClient · Downloader · Unzipper · KvStore · PermissionGuard ·
│ LocalUploadServer · FileSystem · TaskScheduler
├── contracts/ (HAR) # 横切:契约类型(SSOT)
│ └── ets/ Handlers.ets(枚举所有handler名) · dto/*.ets(数据结构) · Errors.ets
└── common/ (HAR) # 横切:Logger/Tracer/EventBus/DI/Result<T>
```
> **为何这样切**`feature_bridge` 与 `contracts` 不含任何业务,可被未来其他 H5 壳复用;`feature_capabilities` 可按渠道裁剪(如海外版去掉微信支付);`domain_resource`/`platform` 可单测。
---
## 5. 核心一:JsBridge 桥引擎设计
> 这是"H5 零改动"的命门。目标:**100% 复刻 lzyzsd/JsBridge 协议**(《契约规范》§2),但用 ArkTS 优雅实现。
### 5.1 协议落地(与契约逐条对应)
| 协议要素 | 实现 |
|---|---|
| 注入 `WebViewJavascriptBridge.js` | 在 `Web().onPageEnd` 回调里 `controller.runJavaScript(bridgeJs)`;JS 文件**直接复用库原文件**,保证 `window.WebViewJavascriptBridge` API 与事件 `WebViewJavascriptBridgeReady` 完全一致 |
| H5→原生(`yy://` | `Web().onLoadIntercept`(拦截范围含 **iframe 导航**,正好匹配 H5 用 `iframe.src='yy://…'` 发起调用)中:`URLDecode` 后,`yy://return/` 前缀→`handleReturnData`(回执/队列);其余 `yy://` 前缀→`flushMessageQueue()`;返回 `true` 拦截,其余返回 `false` 放行 |
| 取队列 `_fetchQueue()` | ⚠️ **队列不靠 `runJavaScript` 返回值取回**`flushMessageQueue` 先以函数名 `_fetchQueue` 预登记回调,再 `runJavaScript("WebViewJavascriptBridge._fetchQueue();")`(不读返回值);H5 内 `_fetchQueue``iframe.src` 置为 `yy://return/_fetchQueue/<队列JSON>`**再次经 `onLoadIntercept``handleReturnData`** 路由到该回调。与 Android `BridgeWebView` 实现完全一致 |
| 原生→H5 单条 | `controller.runJavaScript("WebViewJavascriptBridge._handleMessageFromNative('"+json+"');")` |
| Message 结构 | `{handlerName, data, callbackId, responseId, responseData}`(§2.4),`MessageCodec` 负责与 lzyzsd 完全一致的转义(注意原库对 `\``"` 的二次转义) |
| callbackId 生成 | `JAVA_CB_<自增>_<时间戳>`(格式可自定,H5 只原样回带) |
| 启动消息队列 | 页面 `onPageEnd` 前 native 若 callHandler,先入 `startupMessage` 队列,注入桥 JS 后补发(§2.6) |
> ⚠️ **M1 必须先验证的高风险点**:不同 ArkWeb 版本对**自定义 scheme`yy://`)的 iframe 导航**是否稳定触发 `onLoadIntercept` 存在差异。M1 桥引擎自测时,**首先**用最小回显 H5 确认 `yy://` 能被 `onLoadIntercept` 捕获;若个别版本不触发,回退方案优先级:① `onInterceptRequest` + `WebSchemeHandler`(注册自定义 scheme,能力更强、可取 POST 体);② 极端情况下改桥协议为 `javaScriptProxy` 注入一个 `_bridgeNative.postMessage(json)` 同步方法替代 `yy://` 通道(**此法需同步改写注入的 `WebViewJavascriptBridge.js` 的 `_doSend`/`_fetchQueue`,但对 H5 仍透明,`window.WebViewJavascriptBridge` 对外 API 不变**)。三种方案对 H5 均零改动。|
### 5.2 BridgeController(桥控制器,每个 Bridge 容器持有一个)
职责:管 WebviewController、URL 拦截分发、handler 注册、出入站消息编解码与回调表。**对外暴露 `registerHandler` / `callHandler`,与 Android 端 API 同名同义**。
```typescript
// feature_bridge —— 设计级伪代码(ArkTS 风格,省略 import
export type BridgeHandler = (data: string, callback: (resp: string) => void) => void;
export class BridgeController {
private controller: webview.WebviewController;
private registry: HandlerRegistry; // 注入:能力层填充
private responseCallbacks = new Map<string, (d: string)=>void>();
private startupMessages: Message[] | null = []; // 页面就绪前的积压
private uniqueId = 0;
// —— H5 → 原生:在 Web().onLoadIntercept 调用 ——
onLoadIntercept(url: string): boolean {
const u = decodeURIComponent(url);
if (u.startsWith('yy://return/')) { this.handleReturnData(u); return true; }
if (u.startsWith('yy://')) { this.flushMessageQueue(); return true; }
return false; // 正常导航
}
// —— 原生 → H5:能力层/容器调用 ——
callHandler(name: string, data: string, cb?: (resp: string)=>void): void {
const m = new Message(); m.handlerName = name; m.data = data;
if (cb) { const id = `JAVA_CB_${++this.uniqueId}_${SystemClock.now()}`;
this.responseCallbacks.set(id, cb); m.callbackId = id; }
this.queue(m);
}
registerHandler(name: string, h: BridgeHandler): void { this.registry.put(name, h); }
// —— 页面就绪:在 Web().onPageEnd 调用 ——
onPageEnd(): void {
this.controller.runJavaScript(BRIDGE_JS); // 注入桥
if (this.startupMessages) { this.startupMessages.forEach(m => this.dispatch(m));
this.startupMessages = null; }
}
// —— ⚠️ 关键:触发 H5 交出待发队列。注意队列【不是】靠 runJavaScript 返回值取回,
// 而是 _fetchQueue() 在 H5 内把 iframe.src 置为 'yy://return/_fetchQueue/<data>'
// 再次被 onLoadIntercept 捕获 → handleReturnData 路由到这里注册的 '_fetchQueue' 回调。
private flushMessageQueue(): void {
// 以函数名 '_fetchQueue' 作为 key 预登记回调(与 callbackId 共用同一张 responseCallbacks 表)
this.responseCallbacks.set('_fetchQueue', (queueJson: string) => {
const list = MessageCodec.toArray(queueJson); // H5 待发消息数组
for (const m of list) {
if (m.responseId) { // 是 H5 对"原生 callHandler"的回执
this.responseCallbacks.get(m.responseId)?.(m.responseData);
this.responseCallbacks.delete(m.responseId);
} else { // 是 H5 主动 callHandler
const respFn = m.callbackId
? (d: string) => this.queue(Message.response(m.callbackId!, d))
: (_: string) => {};
const handler = this.registry.get(m.handlerName) ?? this.registry.default();
handler(m.data, respFn); // ← 分发到能力层
}
}
});
// 仅触发,不读取返回值
this.controller.runJavaScript('WebViewJavascriptBridge._fetchQueue();');
}
// —— H5 → 原生 的回执通道:yy://return/<functionName>/<data> ——
// 既处理 _fetchQueue(队列回传),也处理普通 callbackId 回执
private handleReturnData(url: string): void {
const fn = parseFunctionFromReturnUrl(url); // 如 '_fetchQueue' 或 'JAVA_CB_x_y'
const data = parseDataFromReturnUrl(url);
const cb = this.responseCallbacks.get(fn);
if (cb) { cb(data); this.responseCallbacks.delete(fn); }
}
private queue(m: Message): void {
if (this.startupMessages) this.startupMessages.push(m); else this.dispatch(m);
}
private dispatch(m: Message): void {
const json = MessageCodec.escape(m.toJson());
this.controller.runJavaScript(`WebViewJavascriptBridge._handleMessageFromNative('${json}');`);
}
}
```
> 设计要点:
> - `BridgeController` **完全不 import 任何能力**;能力通过 `registry` 注入。桥引擎成为独立 HAR,可复用、可单测。
> - **务必忠实复刻"队列经 `yy://return/_fetchQueue/` 回传"的机制**,不要图省事改读 `runJavaScript` 的返回值——除非你同时改写注入的 `WebViewJavascriptBridge.js`(不推荐,破坏与成熟协议的一致性,易在边界场景出错)。`responseCallbacks` 一张表同时承载"函数名 key`_fetchQueue`"与"callbackId key`JAVA_CB_*`"两类回执,与 Android 端实现完全一致。
> - 上文 `this.controller: webview.WebviewController` 仅为示意。**为可测试性,桥应依赖抽象 `IWebController`(见 §5.6),由容器注入真实 `WebviewController` 适配器**,单测时注入 mock。
> - **必须设置 `DefaultHandler`(空实现)**`flushMessageQueue` 分发时,`handler = registry.get(name) ?? registry.default()`。任何**未注册**的 handler 名都落到默认空实现而**不抛错**(对齐 Android `BridgeWebView.defaultHandler`)。这是"H5 调用永不报错"的最后防线,也是暂缓能力(§6.5)的安全网底座。
### 5.5 线程模型(P0,必须遵守)⚠️
> Android 原版 `BridgeWebView.dispatchMessage` 明确要求**只有在主线程才下发消息**(源码 `Thread.currentThread()==Looper.getMainLooper()` 才 `loadUrl`)。HarmonyOS 同理:**`runJavaScript` 只能在 UI(主)线程调用**。而能力回调(定位 `locationChange`、支付 SDK 回调、传感器、各类系统广播、TaskPool 完成)**经常发生在非 UI 线程**,若直接 `callHandler` → `runJavaScript` 会抛异常或行为异常。
**强制规则**
1. **所有出站下发(`dispatch`/`runJavaScript`)必须在 UI 线程执行**`BridgeController` 内部对 `dispatch` 做线程守卫:非 UI 线程则投递到 UI 线程任务队列。
2. **入站分发(`flushMessageQueue` 触发的 handler 回调)默认就在 UI 线程**`onLoadIntercept` 在 UI 线程回调),handler 内若开了子线程做重活,回主线程再 `callback`/`callHandler`
3. 能力 Provider 的异步事件回传统一经 `bridge.callHandler`,由桥内部保证线程切换,**Provider 不需自己关心线程**。
```typescript
// BridgeController 内:UI 线程守卫(示意)
private uiContext: UIContext; // 由容器在 aboutToAppear/onPageShow 注入
private dispatch(m: Message): void {
const run = () => {
const json = MessageCodec.escape(m.toJson());
this.controller.runJavaScript(`WebViewJavascriptBridge._handleMessageFromNative('${json}');`);
};
if (isOnUiThread()) run();
else this.uiContext.runScopedTask(run); // 或 emitter/AppStorage 投递回 UI 线程
}
```
> 实现方式可选:① UIContext 的 UI 线程任务;② `@ohos.events.emitter` 在 UI 线程订阅、能力线程 emit;③ `AppStorage`/`LocalStorage` 状态驱动。无论哪种,**对外保证:能力随便在哪个线程 `callHandler`,最终都在 UI 线程 `runJavaScript`**。
### 5.6 IWebController 抽象(P2,支撑单测)
桥不直接绑死系统类,依赖最小接口,便于 mock 与未来替换内核:
```typescript
export interface IWebController { // 桥只用到这几个能力
runJavaScript(script: string): void;
runJavaScript(script: string, cb: (err: Error|null, ret: string)=>void): void;
loadUrl(url: string): void;
}
// 生产:WebviewControllerAdapter implements IWebController(包一层 webview.WebviewController
// 测试:FakeWebController implements IWebController(记录脚本、模拟 yy:// 回执)
```
> 这样 `feature_bridge` HAR 不 import `@kit.ArkWeb` 的具体类,`BridgeController`/`MessageCodec`/`VersionResolver` 均可纯逻辑单测,§13 的 ≥80% 覆盖率目标方可达成。
### 5.3 同步返回 vs 异步推送
《契约规范》区分两类返回,框架用同一套 `callback` 表达:
- **同步返回**(如 `getTime`/`getnetwork`/`getlocationinfo` 主动取):handler 内立即 `callback(value)`
- **异步推送**(如定位结果、支付回调):能力层在事件到达时调用 `bridge.callHandler('出站名', data)`
### 5.4 数据载荷的"裸字符串 vs JSON"陷阱
`MessageCodec` 不擅自 JSON 化 data。能力层按《契约规范》§8/§9 **逐 handler** 决定:`getTime` 回裸串、`getwifiLevel` 回 JSON 串。`contracts` 层为每个 handler 声明 `payloadType: 'raw' | 'json'` 与 DTO,编译期约束,杜绝写错。
---
## 6. 核心二:能力插件框架(CapabilityProvider
> 让"新增/裁剪一个原生能力"成为零侵入操作,是框架成熟度的体现。
### 6.1 插件接口
```typescript
export interface CapabilityProvider {
readonly name: string; // 如 'location'
// 把本能力的入站 handler 注册到桥;并持有 bridge 以便异步推送
register(bridge: BridgeController, ctx: CapabilityContext): void;
onForeground?(): void; onBackground?(): void; // 生命周期联动
onDestroy?(): void;
}
export interface CapabilityContext {
uiAbilityContext: common.UIAbilityContext;
config: ConfigManager; platform: PlatformServices; log: Logger;
}
```
### 6.2 注册表 + 组装根(DI
```typescript
// entry/di/AppModule.ets —— 组装根:唯一知道"全部能力"的地方
export function buildCapabilities(): CapabilityProvider[] {
return [
new ShareProvider(), new LoginProvider(), new PayProvider(),
new LocationProvider(), new AudioProvider(), new ShakeProvider(),
new DeviceProvider(), new ClipboardProvider(), new NetworkProvider(),
new VibrateProvider(), new ScanProvider(), new CameraProvider(),
new PhotoProvider(), new NavProvider(), // OpenurlTitleData/SwitchOverGameData/backgameData
new AppSystemProvider(), // orientation/browser/finsh/openApplyDownloadpath/notification
// —— 本期暂不集成:用占位桩注册,保证 H5 调用不报错(见 §6.5),未来换回真实 Provider ——
new RoomStubProvider(), // 视频房(声网)暂缓 → 真实版 RoomProvider
new PayStubProvider(), // 支付暂缓 → 真实版 PayProvider
// 闲聊:无 H5 桥接口,无需注册(默认 handler 兜底)
];
}
// 容器初始化时:
capabilities.forEach(p => p.register(bridge, ctx));
```
#### 44 个入站 handler → Provider 归属(确保零孤儿、可逐条核对《契约规范》§8)
| Provider | 负责的入站 handler |
|---|---|
| `ShareProvider` | `friendsSharetypeUrlToptitleDescript` |
| `LoginProvider` | `accreditlogin` |
| `PayProvider` (本期=`PayStubProvider` 桩,§6.5 | `paybrowser``getGameplay`(旧版可选) |
| `LocationProvider` | `startlocation``getlocationinfo` |
| `AudioProvider` | `prepareaudio``mediaTypeAudio``srcIsloop``voicePlaying` |
| `ShakeProvider` | `startshake``SwitchShake``stopshake` |
| `DeviceProvider` | `getTime``getphonestate``getbattery``getwifiLevel``getcompareCode``getphoneInfo``getmarketname``getothername``getOther` |
| `ClipboardProvider` | `gameCopytext``gamepastetext` |
| `NetworkProvider` | `getnetwork` |
| `VibrateProvider` | `vibrator``repeatvibrator``canclevibrator` |
| `ScanProvider` | `opensaoma` |
| `CameraProvider` | `opencamera` |
| `PhotoProvider` | `getphoto` |
| `RoomProvider` (本期=`RoomStubProvider` 桩,§6.5 | `createRoom``exitRoom``getVideoinfo``DragViewvideoIsshow` |
| `NavProvider` | `OpenurlTitleData``SwitchOverGameData``getGameinstall``backgameData`(New) |
| `AppSystemProvider` | `orientation``browser``finsh``openApplyDownloadpath``notification`(空实现) |
> 合计 **44 个入站 handler 全部有且仅有一个归属**。出站 handler(§9)由对应 Provider 在事件到达时 `bridge.callHandler` 推送(如 `LocationProvider→getlocationinfo`、`PayProvider→PayuserPaytypePaystate`、`AudioProvider→getaudiourl/gameui_*`、`DeviceProvider→getBattery/getwifiLevel/phonestate`、`ScanProvider→getsaomaData`、`CameraProvider→getcameraaAddress`、`PhotoProvider→getphoto`、`ShareProvider→sharesuccess`、`LoginProvider→sharelogin`、`ShakeProvider→shakeEnd`、`NavProvider→getWebdata`、容器→`appservice/setPostUrl`)。
### 6.3 一个 Provider 的样板(以定位为例)
```typescript
export class LocationProvider implements CapabilityProvider {
readonly name = 'location';
private bridge!: BridgeController; private ctx!: CapabilityContext;
register(bridge: BridgeController, ctx: CapabilityContext): void {
this.bridge = bridge; this.ctx = ctx;
bridge.registerHandler(H.startlocation, (data, _cb) => this.start(data === '1'));
bridge.registerHandler(H.getlocationinfo, (_d, cb) => cb(this.lastOrError()));
}
private start(continuous: boolean): void {
geoLocationManager.on('locationChange', loc => {
this.bridge.callHandler(H.getlocationinfo, JSON.stringify(toMapLocationInfo(loc)));
if (!continuous) geoLocationManager.off('locationChange');
});
}
private lastOrError(): string { /* 返回 MaplocationInfo JSON 或 {errorCode:-1,...} */ }
}
```
> 同理:`ShareProvider`/`LoginProvider`/`PayProvider` 对接微信/QQ/抖音/支付 HarmonyOS SDK`AudioProvider` 用 `AVPlayer`/`SoundPool``ScanProvider` 用 Scan Kit`CameraProvider` 用 Camera Picker`DeviceProvider` 用 deviceInfo/telephony/`@ohos.batteryInfo`/`@ohos.wifiManager`。**所有 handler 名、参数、回传结构严格照《契约规范》§8/§9/§12。**
>
> **`ShareProvider` 的"截图分享"内部流程**(契约 §10.1 的 `type=="2"`):`friendsSharetypeUrlToptitleDescript` 收到 `type=="2"` 时,由 `ShareProvider` 内部 `runJavaScript("...canvas.toDataURL...")` 取当前页 Canvas 的 base64(失败可回退原生截图),再经平台层 `LocalUploadServer`(其地址此前已由 `setPostUrl` 告知 H5)上传/或直接交分享 SDK。此为**内部实现**,不新增对外 handler,对 H5 透明。
### 6.4 补齐项以"新增 Provider/增强"落地(《契约规范》§14.2)
| 待补齐 | 设计方案 |
|---|---|
| QQ/抖音分享结果回传 | `ShareProvider` 统一在分享 SDK 回调里 `callHandler('sharesuccess', {success,type})`,补齐三端一致 |
| 抖音/快手登录 | 新增 `DouyinLoginProvider`,对接开放平台,**回传 code**(而非用户资料),由服务端换 token |
| 主动截图 | 新增入站 handler `getCanvasBase64`(向后兼容、H5 可选用),`runJavaScript``canvas.toDataURL` 取回 base64 |
| AppSecret/API_KEY 明文 | **下沉服务端**:登录走 code 模式,支付签名由服务端出,客户端不存密钥 |
### 6.5 暂不实现的能力:占位桩(No-op Stub),保证 H5 调用不报错/不卡死
> **范围**:本期**视频房**、**支付**、**闲聊**先不集成。但**接口必须"留空"**——即仍把对应 handler 注册进桥,只是实现为空操作。否则 H5 调用时行为不确定,且若 H5 传了 `responseCallback` 还可能**永久挂起**。
**两道安全网(缺一不可)**
1. **桥默认 handler 兜底**`BridgeController` 必须设置一个 `DefaultHandler`(空实现),任何**未注册**的 handler 名都路由到它,不抛错(对齐 Android `BridgeWebView.defaultHandler`)。这是最后防线。
2. **显式占位 Provider**:对"明确暂缓"的能力,提供 `StubProvider` 显式注册占位 handler——行为确定、可日志、可灰度开启,优于依赖默认兜底。
**占位桩的实现规则**
- **void 型 handler**(H5 不期望同步返回):空实现(可选 `log`/轻提示"功能暂未开放"),**不要**调用任何 `callHandler` 出站。
- **有同步返回的 handler**H5 传了 `responseCallback`):**必须回一个安全默认值**,否则 H5 卡死。如返回 `'0'`、空串 `''` 或合法空 JSON。
- **绝不**触发该能力的出站 handler(如不 `callHandler('getVideoinfo')`、不 `callHandler('PayuserPaytypePaystate')`),H5 收不到推送即按"无事发生"处理。
**本期占位清单**
| 能力 | 占位 Provider | 入站 handler(注册为桩) | 桩行为 | 关联出站(**不触发**) |
|---|---|---|---|---|
| 视频房(声网) | `RoomStubProvider` | `createRoom``exitRoom``getVideoinfo``DragViewvideoIsshow` | 全部 void,空实现 + 日志 | `getVideoinfo`(uid 推送) |
| 支付 | `PayStubProvider` | `paybrowser``getGameplay`(旧) | void,空实现(可轻提示"支付暂未开放") | `PayuserPaytypePaystate` |
| 闲聊 | —— | **无 H5 接口**(《契约规范》§10.4`sgapi` 是闲聊且全注释、无 `registerHandler`) | 无需桩;H5 若误调,走默认 handler 兜底 | —— |
```typescript
// 占位 Provider 样板:注册即留空,H5 调用安全无副作用
export class RoomStubProvider implements CapabilityProvider {
readonly name = 'room(stub)';
register(bridge: BridgeController, ctx: CapabilityContext): void {
const noop: BridgeHandler = (_d, _cb) => { ctx.log.i('room not integrated yet'); };
bridge.registerHandler(H.createRoom, noop);
bridge.registerHandler(H.exitRoom, noop);
bridge.registerHandler(H.getVideoinfo, noop);
bridge.registerHandler(H.DragViewvideoIsshow, noop);
}
}
export class PayStubProvider implements CapabilityProvider {
readonly name = 'pay(stub)';
register(bridge: BridgeController, ctx: CapabilityContext): void {
bridge.registerHandler(H.paybrowser, (_d, _cb) => ctx.log.i('pay not integrated yet')); // 可 Toast 提示
}
}
```
> **组装根切换**`buildCapabilities()` 里把 `new RoomProvider()`/`new PayProvider()` 暂时替换为 `new RoomStubProvider()`/`new PayStubProvider()`。**契约 handler 名/数量不变**,未来集成时换回真实 Provider 即可,**对 H5 始终零改动、零报错**。
> **闲聊**:本就没有 H5 桥接口,无需任何桩;默认 handler 兜底足以保证任何误调用不报错。
---
## 7. 容器设计:双容器模型
《契约规范》明确有**两套**互不相同的 H5↔原生机制,框架对应**两类容器组件**。
### 7.1 BridgeGameContainer(大厅 + 子游戏)
对应 `webviewActivity`/`NewwebviewActivity`。基于桥协议。
```typescript
@Component
export struct BridgeGameContainer {
private controller = new webview.WebviewController();
private bridge = new BridgeController(this.controller);
// ⚠️ 仅 debug 包开启远程调试;release 包必须关闭(安全加固,见附录 B)
aboutToAppear(): void { if (BuildProfile.DEBUG) webview.WebviewController.setWebDebuggingAccess(true); }
build() {
Web({ src: this.entryUrl(), controller: this.controller })
.javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
.mixedMode(MixedMode.All).cacheMode(CacheMode.None)
.geolocationAccess(true).zoomAccess(false)
.onControllerAttached(() => { /* 注入对象(本容器不需要)、设 UA(默认即可) */
buildCapabilities().forEach(p => p.register(this.bridge, ctx)); })
.onLoadIntercept((e) => this.bridge.onLoadIntercept(e.data.getRequestUrl()))
.onPageEnd(() => { this.bridge.onPageEnd(); }) // 仅注入桥 + 补发启动队列
.onProgressChange((e) => { if (e.newProgress === 100 && !this.firstDone) {
this.firstDone = true; // 首次到 100% 才推
this.bridge.callHandler('appservice', '1'); // 前台
this.bridge.callHandler('setPostUrl', uploadUrl()); } })
}
// 子游戏切换 SwitchOverGameData → 仅 controller.loadUrl(新目录 index.html?Launchtype=1)
}
```
> ⚠️ **时序铁律**`app_data.js` 必须在本容器**加载页面之前**就由 `AppDataInjector` 写入大厅目录(见 §8.4,发生在 `StartupOrchestrator` 的 `INJECT_APP_DATA` 步、早于进入本容器)——因为 H5 首页用 `<script src="app_data.js">` 在**加载时同步读取**。**切勿**放到 `onPageEnd`/`onProgressChange` 里推,那时页面已加载完、H5 早已读过 `app_data.js`,为时已晚。`setPostUrl`/`appservice` 则按契约在首次进度 100% 时下发(与 Android `onProgressChanged==100` 一致)。
- **入口 URL**`weburl` 空 → `file://<解压根>/gamehall/index.html?Launchtype=0`;非空 → `http://<weburl 去-换/>?Launchtype=0`(《契约规范》§3/§5)。
- **前后台**`UIAbility.onForeground/onBackground``callHandler('appservice','1'|'2')`
- **子游戏切换**(路径 A`SwitchOverGameData`):M2/M4 先用同容器 `controller.loadUrl()` 到目标目录跑通;**M5 演进为双 Web 保活模型(见 §9)**。
> **🔴 大厅/子游戏激活模型(M5 确认,2026-06-25**
> - **双 Web 槽****大厅 Web 常驻**;**子游戏 Web 进入时创建、返回大厅时销毁**(子游戏不保活)。
> - **同时只有一个激活**(激活=联网+收发包+界面更新):进子游戏 → 大厅 Web `onInactive()` **彻底停网络+渲染**;返回 → 销毁子游戏 Web、大厅 `onActive()` 恢复(返回快、保留大厅状态/连接)。
> - **切换拓扑**:仅 大厅↔子游戏,**无 子游戏→子游戏**(只需一个子游戏槽)。
> - **各自独立桥+能力**:大厅 Web 与子游戏 Web 各持一套 `BridgeController` + `buildCapabilities()` 注册;激活者响应。
> - 注:`onInactive()` 暂停 JS/渲染/多数活动;H5 自开的 WebSocket 在原生层未必完全断(H5 零改动下最接近的原生手段)。
> - M2/M4 的单 WebView `loadUrl` 切换是过渡实现,M5 用 `NodeController`/`BuilderNode` 离屏保活大厅 + 临时子游戏 Web 落地此模型。
### 7.2 GenericWebContainer(通用网页容器)
对应 `openwebActivity1`**不挂桥**,用 `javaScriptProxy` 注入 `settings` 对象 + `runJavaScript` 直调。
```typescript
@Component
export struct GenericWebContainer {
private controller = new webview.WebviewController();
// settings:H5→原生 6 方法(《契约规范》§11.3)
private settings = new SettingsProxy(this.controller, /*onReturnData*/ (data)=> this.finishWith101(data));
build() {
Web({ src: this.params.url, controller: this.controller })
.javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
.geolocationAccess(true).zoomAccess(false)
.cacheMode(this.online ? CacheMode.Default : CacheMode.Only) // 有网:按HTTP缓存策略(≈LOAD_DEFAULT);无网:只用缓存(≈LOAD_CACHE_ELSE_NETWORK)。非关键项
.javaScriptProxy({
object: this.settings, name: 'settings',
methodList: ['backgameData','loadurl','browser','finishweb','isexitdialogeshow','isbackfinishweb'],
controller: this.controller,
})
.onProgressChange((e) => { if (e.newProgress === 100 && !this.loaded) {
this.loaded = true;
this.controller.runJavaScript(`getWebdata('${this.params.data}')`); } }) // 原生→H5 直调
}
// 返回键:若 H5 调过 isbackfinishweb() → runJavaScript('gamebackkeydown()');退出确认 → runJavaScript('backgameData()')
private finishWith101(data: string) { /* 把 data 经路由/emitter 回传上层 Bridge 容器 → 上层 callHandler('getWebdata', data) */ }
}
```
- `settings` 的 6 方法、`getWebdata`/`gamebackkeydown`/`backgameData` 三个直调函数严格照《契约规范》§11.3。
- **回传上层**:用路由返回值或 `emitter` 事件把 `data` 投递回打开它的 Bridge 容器,由后者 `callHandler('getWebdata', data)`(复刻 Android 结果码 101 语义)。
### 7.3 file:// 跨域(V2 已验证,2026-06-25
H5 以 `file://` 加载并请求本地资源时,ArkWeb 默认按 CORS 拦截(file 页面 origin 为 `null`,且 `file` 不在允许跨域的 scheme 列表)——**T-M2-08 真机首测确认了这一拦截**(`Access to fetch ... blocked by CORS policy`)。
**采用的解决方案(落地、对 H5 透明)**:在 `onControllerAttached` 调用
`WebviewController.setPathAllowingUniversalAccess([<filesDir>/tsgames])`,把资源根加入"允许跨域访问"白名单——该路径下的 `file://` 资源即放开同源限制,H5 的 XHR/fetch 正常工作。约束:路径须为 `filesDir`/`resourceDir` 子目录、与用户文件隔离;一旦设置,`file` 协议仅限访问白名单内资源(覆盖 `fileAccess` 行为)。
> 此方案比通用的 `onInterceptRequest`/`WebSchemeHandler` 代理更简(无需逐请求拦截、无需改首页 scheme),是 HarmonyOS 官方《Web 页面跨域解决方案·本地资源跨域》的推荐做法。若未来遇白名单不可用的边缘场景,仍可回退 `onInterceptRequest` 自定义响应。**对 H5 始终零改动**。
### 7.4 屏幕方向(横屏项目)
本项目为**横屏项目**:应用**默认横屏、不跟随设备传感器自动旋转**;同时**遵守 H5 零改动铁律**,完整保留 H5 经方向接口显式切换(含竖屏)的能力,与原 Android 一致(《契约规范》§7 屏幕方向约束 / §8.4 / §11)。落地方式:
- **横屏自动旋转**`EntryAbility``module.json5``"orientation": "auto_rotation_landscape"`(在横屏两朝向间跟随重力传感器自动旋转,但不进入竖屏)。
- **H5 动态切换(保留竖屏能力)**:方向接口收到请求时用窗口 API **动态覆盖**,不写死锁定——
- `AppSystemProvider``orientation` handler`data === '1'` → 横屏,否则 → 竖屏(对齐 Android `setRequestedOrientation`);
- 子游戏 `SwitchOverGameData`:按 `webtype``"3"` 横 / `"2"` 竖)设方向后再 `loadUrl`
- `GenericWebContainer`:按打开参数 `orientation``"1"` 竖 / 其他 横)设方向。
- **实现 API**:运行时 `window.Window.setPreferredOrientation(orientation: window.Orientation)` 动态切换,`window.Orientation``LANDSCAPE` / `PORTRAIT`(编码前用 `devecocli docs` 核对签名)。
- **要点**`module.json5``auto_rotation_landscape` 提供"横屏内跟随传感器旋转(不进竖屏)",运行时 `setPreferredOrientation` 提供"H5 显式切换"——二者叠加既满足横屏项目定位,又守住 H5 零改动。
### 7.5 双 Web 大厅/子游戏架构(M5 定稿,2026-06-25 验证通过)
> 复刻原 Android 双 Activity 语义(webviewActivity 大厅 + NewwebviewActivity 子游戏,靠 Activity 生命周期 onPause/onResume 暂停/恢复),在鸿蒙用**单 Ability + 单页内两个 Web 组件**实现——更轻、切换更快、可离屏预热。M2/M4 的单 WebView `loadUrl` 切换是过渡实现,M5 替换之。
**结构**`BridgeGameContainer``Stack { 大厅Web(常驻) ; if(子游戏!=null) 子游戏Web }`,由 **WebSlotManager** 持两 slot、维护 activeSlot、执行激活协议。每个 slot = 独立 `WebviewController + BridgeController + buildCapabilities()`。子游戏 `SwitchOverGameData` 创建、`backgameData` 销毁(不保活)。大厅常驻无需 NodeController(两 Web 同在一页)。
**需求①——大厅/子游戏接口不串(结构隔离)**:每个 Web 一套独立桥 + 能力实例,`BridgeController#L` 只 runJavaScript 到大厅 Web、`#S` 只到子游戏 Web,**物理不可能串**。唯一共享单例(`WeChatApi`)与 EventBus 导航事件由 WebSlotManager **路由到 activeSlot**
**需求②——同时只有一个激活**(激活=联网+渲染+声音+桥接口可执行)。激活协议四联动:
| 维度 | 激活端 | 非激活端 |
|---|---|---|
| 渲染/动画/定位 | `web.onActive()` + 显示 | `web.onInactive()` + 隐藏 |
| 桥接口 | `bridge.setActive(true)` | `bridge.setActive(false)`:入站忽略、出站抑制 |
| 原生能力监听/播放 | `registrar.forEachForeground()` | `registrar.forEachBackground()`:停传感器/定位/网络监听、停音频 |
| H5 自身联网/心跳/声音 | `callHandler('appservice','1')` | `callHandler('appservice','2')`H5 按契约自行进入后台态 |
> ⚠ **平台限制与正解**ArkWeb `onInactive()` 暂停动画/定位但**不暂停 JS**;能停 JS 定时器的 `pauseAllTimers()` 是**全局**(会停两个 Web,不可用于单 Web)。即鸿蒙**没有"只冻结一个 Web 的 JS/网络"的原生开关**(比 Android WebView.onPause 弱)。正解:原生侧停掉能停的(渲染/能力/桥),**H5 侧用契约现成的 `appservice('2')` 自停网络/声音**——与原 Android 一致、H5 零改动。
**需求③——cookie + localStorage 共享(已真机验证)**
- **localStorage**:✅ **已验证共享**——两个不同 file:// 路径(`filesDir/tsgames/lsA``/lsB`+ `setPathAllowingUniversalAccess([filesDir/tsgames])`,一页写、另一页读到同值。故**沿用 file:// 主选**即满足。
- **cookie**file:// 页面不支持 WebView cookiecookie 绑定 http(s) scheme);但原 Android 的 cookie 全在 native HTTP 层(Volley/java.net.CookieManager),**WebView 从未开 file-scheme cookieH5 不用 WebView cookie**——故非需求缺口。若未来 H5 在 http(s) 用 cookie`WebCookieManager` 为**应用级全局单例**(所有 Web 组件共享)自动满足。
- 结论:**采用 file:// + setPathAllowingUniversalAccess + WebCookieManager(全局)**,无需自定义协议。
**激活状态机**WebSlotManager):
```
进子游戏: deactivate(大厅)[appservice'2'→onInactive→forEachBackground→setActive(false)→隐藏]
→ 注入子游戏 app_data(launchtype'1') → 建子游戏 slot → activate(子游戏)[显示→onActive→forEachForeground→setActive(true)→100%时 appservice'1'+setPostUrl]
返回大厅: destroy(子游戏)[forEachBackground→bridge.dispose→销毁 Web] → activate(大厅)[显示→onActive→forEachForeground→setActive(true)→appservice'1'+getWebdata(data)]
App 前后台: 只对 activeSlot 下发 appservice
```
**改造清单**:新增 `WebSlotManager`(entry)`BridgeController``setActive()``BridgeGameContainer` 改 Stack 双 Web、SWITCH_GAME/BACK_GAME 走 SlotManagerProvider 落实 `onForeground/onBackground`Shake/Location/Network 停起监听、Audio 停起播放);`WeChatApi` resp 路由到 activeSlot。
---
## 8. 启动编排与配置/资源子系统
### 8.1 StartupOrchestrator(启动状态机)
把《契约规范》§3 时序实现为**显式状态机**,每步可重试、可观测、可降级:
```
INIT → LOAD_LOCAL_CONFIG → REQUEST_PERMISSIONS → FETCH_REMOTE_CONFIG
→ RESOLVE_VERSION → [PREPARE_RESOURCE: 内置拷贝/zip下载解压] → INJECT_APP_DATA
→ ENTER_HALL (跳 BridgeGameContainer)
任一步失败 → FALLBACK(本地缓存) 或 BLOCK(showmessage 公告)
```
- `showmessage` 非空 → 阻断弹窗(§3.2)。
- 远程配置长度 <30 视为错误文案直接提示。
### 8.2 ConfigManager(配置)
- **本地配置**:弃用"目录名编码",改为随包内置 `resources/rawfile/app_config.json`(KV),提供《契约规范》§4.2 全部键:`agent/channel/gamedir/gamestart/appversion/market/gameid/weburl/gameconfig/other/tuiguang`。对 H5 无感知。
- **远程配置**`HttpClient.get("http://"+gameconfig.replace(/-/g,'/')+".txt?a="+ts)`,禁缓存。
- **分层覆盖**`VersionResolver` 按线上真实格式实现 **agent→game→channel→market** 后层覆盖(§4.4)——单 `agentlist` 树、资源字段 `game_zip`、数字 id/版本归一、gameid 为空回退 `version.xml``<game id>`,无二级 `url` 二次请求。纯函数实现,便于单测。
### 8.3 ResourceManager(资源)
```
路径(@ohos.file.fs + context.filesDir):
解压根 urlpath = <filesDir>/tsgames/<bundle>/<时间戳>/<gamedir>
游戏目录 = <urlpath>/gamehall
入口 = file://<urlpath>/gamehall/index.html
KV 持久化 urlpath / upurlpath@ohos.data.preferences
流程:
首启 → 拷贝内置包(rawfile) → @ohos.zlib.decompressFile 解压
非首启 → 比较 version.xml<version value> 与远程 game_version
需更新 → Downloader 下载 game_zip(?a=ts) → 删旧 → decompressFile → 收尾
```
- 下载、解压在 **TaskPool/Worker** 执行(§9),UI 线程只收进度回调。
- `version.xml` 解析用 `@ohos.xml`XmlPullParser),取 `agent/game/channel/version`
### 8.4 AppDataInjector
加载大厅前,在 `<urlpath>/gamehall/app_data.js` 写入《契约规范》§6 的全局变量(`app_version/app_agent/app_market/...`)。用 `fs` 写文件即可;**注意必须在 `Web` 加载该页面之前完成**(在 StartupOrchestrator 的 INJECT_APP_DATA 步,先于 ENTER_HALL)。
---
## 9. 性能架构
> "高性能"落在四个可量化抓手上。
| 抓手 | 设计 | 收益 |
|---|---|---|
| **Web 保活 / 预热**(确认模型见 §7.1 | **双 Web 槽**:大厅 Web 常驻(`NodeController`/`BuilderNode` 离屏保活),子游戏 Web 进入创建/返回销毁;**同时只一个激活**——进子游戏时大厅 `onInactive()` 停网络+渲染,返回时大厅 `onActive()` 恢复并销毁子游戏 Web;各 Web 独立桥+能力;无子游戏→子游戏 | 子游戏进入<300ms、返回大厅秒回且保留状态/连接、非激活端不占网络/CPU |
| **并发卸载重活** | 下载/解压/解密/MD5 校验放 **TaskPool**(短任务)或 **Worker**(长驻);UI 线程零阻塞 | 启动期不卡顿,进度流畅 |
| **JSBridge 批处理** | 沿用 lzyzsd 队列语义:多条出站消息可在一次 `flushMessageQueue` 内聚合;高频 handler(定位/电量)做节流 | 减少 `runJavaScript` 跨引擎调用次数 |
| **资源就绪即渲染** | 本地 `file://` 优先;首屏资源预解压;`onPageEnd` 后再注桥,避免阻塞首屏;图片/音频懒加载 | 首帧更快 |
补充:
- **内存**:容器 `aboutToDisappear` 解绑 Controller、`onRenderExited` 兜底重载(防渲染子进程崩溃白屏)。
- **冷启动**`StartupOrchestrator` 并行化"权限申请"与"内置包拷贝"等无依赖步骤。
- **包体**:能力 Provider 按渠道裁剪(HSP 动态特性)。
### 9.1 TaskPool 并发的 Sendable 约束(P1,必须遵守)⚠️
HarmonyOS `TaskPool``@Concurrent` 任务**跨线程传参/返回值必须是 Sendable**(基本类型、`@Sendable` class、可序列化对象),**不能传**:闭包回调、`WebviewController``UIAbilityContext`、复杂业务对象。因此下载/解压的"进度回传"不能直接传 callback 进 TaskPool。落地范式:
```typescript
// 1) 重活在 @Concurrent 函数里跑,只接收/返回 Sendable 数据
@Concurrent
function unzipTask(zipPath: string, destDir: string): boolean { /* @ohos.zlib.decompressFile */ return true; }
// 2) 进度/完成经 emitter 回 UI 线程(不跨线程传函数)
@Concurrent
function downloadTask(url: string, savePath: string, eventId: number): void {
// 边下边 emitter.emit({eventId}, { data: percent }) ← 仅传 Sendable 数字
}
// UI 线程:emitter.on({eventId}, e => updateProgressUI(e.data.percent));
// taskpool.execute(downloadTask, url, savePath, eventId);
```
> 要点:**TaskPool 只搬 Sendable 数据,UI 反馈一律走 `emitter`/`AppStorage`**。这条不遵守,编译期/运行期必报错。`context.filesDir` 等路径应在 UI 线程取好后以字符串传入。
---
## 10. 横切关注点
| 关注点 | 设计 |
|---|---|
| **契约类型(SSOT** | `contracts` HAR`enum Handlers`(所有 handler 名常量,杜绝拼写漂移如 `finsh`/`getcameraaAddress`/`backgameData-`);每个 DTO 一个 interface(§12 结构);编译期保证 |
| **日志/追踪** | `Logger`(分级)+ `BridgeTracer`:每条桥消息打 `traceId`,可还原"H5调用→handler→回传"全链路 |
| **错误中心** | `Result<T>` 统一返回;`ErrorCenter` 收敛异常并按策略(弹窗/静默/上报) |
| **事件总线** | `EventBus`/`emitter`:前后台、网络变化、容器间回传等解耦 |
| **依赖注入** | 轻量 `DIContainer`(组装根 `AppModule` 集中装配),避免散落 new |
| **权限** | `PermissionGuard` 统一申请存储/电话/定位/相机/麦克风,对齐《契约规范》§13 |
| **安全** | 密钥下沉服务端;`file://` 跨域按官方安全实践;自定义协议白名单 |
| **可测试** | 桥引擎、VersionResolver、MessageCodec 纯逻辑可单测;能力 Provider 以 mock bridge 测注册与回传 |
---
## 11. 契约可追溯性矩阵
> 保证"设计覆盖契约 100%",无遗漏。
| 契约规范条目 | 落地模块 | 验收点 |
|---|---|---|
| §2 桥协议(yy://、_handleMessageFromNative、_fetchQueue、Message、注入时机) | `feature_bridge/BridgeController + MessageCodec + WebViewJavascriptBridge.js` | H5 `registerHandler/callHandler` 全部生效 |
| §8 入站 44 handler | `feature_capabilities/*Provider.register()` | 逐一对照名称/参数/同步返回 |
| §9 出站 ~21 handler | 各 Provider 在事件时 `bridge.callHandler` | 名称/data 结构/时机一致 |
| §11.1 路径 A `SwitchOverGameData` | `BridgeGameContainer` + `NavProvider` | 同容器 loadUrl 切换 |
| §11.2/§11.3 通用网页容器 | `GenericWebContainer + SettingsProxy` | `settings` 6 方法 + 3 直调函数 + 101 回传 |
| §3 启动时序 | `StartupOrchestrator` | 状态机逐步 |
| §4 配置/分层覆盖 | `ConfigManager + VersionResolver` | 单 agentlist 树 · agent→game→channel→market 后层覆盖 · game_zip |
| §5 资源管理 | `ResourceManager + Unzipper + Downloader` | 路径/版本比较/下载解压 |
| §6 app_data.js | `AppDataInjector` | 全局变量同名同义、加载前注入 |
| §7 WebView 能力 | `BridgeGameContainer` 属性 | JS/DOM/file/mixed/cache/geo |
| §10 设备能力 | 各 Provider | 分享/登录/支付/定位/音频/摇一摇/扫码/相机/震动/剪贴板/网络/电量/WiFi/电话 |
| §13 常量/权限 | `ConfigManager + PermissionGuard` | 渠道账号、权限齐备 |
| §14.2 补齐项 | 新增/增强 Provider | QQ/抖音回调、抖音登录、主动截图、密钥下沉 |
---
## 12. 实施路线图(里程碑)
| 阶段 | 交付 | 关键验收 |
|---|---|---|
| **M0 脚手架** | `devecocli create` 多模块工程、CI、签名 | 空壳可跑 |
| **M1 桥引擎(最高优先)** | `feature_bridge` + `IWebController` + 线程守卫 + 回显 handler;注入桥 JS | ① 最小回显 H5:`callHandler('getTime')` 能返回;**② 阻塞性验证:`yy://` 的 iframe 导航确被 `onLoadIntercept` 捕获**(否则立即切 §5.1 回退方案) |
| **M2 容器 + 启动 + 资源** | 双容器、StartupOrchestrator、Config/Resource/AppData | 真机加载现有大厅 H5 `index.html`,能进子游戏;**阻塞性验证:`file://` 加载下 H5 的 XHR/fetch 取本地资源可用**(否则启用 §7.3 自定义协议方案) |
| **M3 能力(本期范围)** | 分享/登录/定位/音频/摇一摇/扫码/相机/震动/剪贴板/网络/设备 等 Provider;**视频房、支付用占位桩(§6.5)**;闲聊不涉及 | 用真实 H5 跑通本期业务;**视频房/支付被调用时无报错、无卡死**(桩 + 默认 handler 兜底);逐条对照可追溯性矩阵 |
| **M4 通用网页容器** | `GenericWebContainer + SettingsProxy` | 活动页/收银台/客服打开、回传正常 |
| **M5 性能 & 加固** | Web 保活、TaskPool、密钥下沉、可观测 | 子游戏切换流畅、冷启动达标、安全审查通过 |
| **M6 暂缓能力集成(按需)** | 把 `RoomStubProvider`/`PayStubProvider` 换回真实 `RoomProvider`/`PayProvider`;如需再加抖音登录等补齐项 | 真实视频房/支付打通,**H5 仍零改动** |
> **强烈建议**:M1 桥引擎必须先用一个**最小回显 H5** 单独验证,再接真实 H5;桥不稳,一切白搭。**两个"阻塞性验证"`yy://` 拦截、`file://` 跨域)必须在 M1/M2 通过**——它们是整个适配最底层的两个不确定性,越早证伪越好。
---
## 13. 验收标准
**功能验收(H5 零改动)**
- [ ] 现有大厅 H5 与全部子游戏 H5 **一行不改**即可运行。
- [ ] 《契约规范》§8 全部入站 handler、§9 全部出站 handler 行为一致(名称/参数/数据结构/时机)。
- [ ] 通用网页容器 `settings` 6 方法 + 3 直调函数 + 101 回传一致。
- [ ] `app_data.js` 全局变量、入口 URL、`?Launchtype=` 参数一致。
- [ ] **暂缓能力(视频房 `createRoom`/`exitRoom`/`getVideoinfo`/`DragViewvideoIsshow`、支付 `paybrowser`/`getGameplay`)被 H5 调用时:不报错、不卡死、不误触发出站推送**(占位桩 + 默认 handler 兜底,§6.5)。
**质量验收**
- [ ] 桥引擎、VersionResolver、MessageCodec 单测覆盖率 ≥80%。
- [ ] 冷启动到大厅首帧、子游戏切换时延达标(建议 <2s / <300ms,按机型基线)。
- [ ] 下载/解压全程不阻塞 UI;渲染崩溃可自恢复。
- [ ] 无客户端硬编码密钥;权限按需申请、合规。
- [ ] 全链路日志可追踪一条桥消息。
---
> **一句话总结**:以 `feature_bridge`100% 复刻 lzyzsd 协议)为基石,能力以 `CapabilityProvider` 插件化挂载,配置/资源/启动以领域服务编排,双容器分别承载"桥协议"与"settings 注入"两套契约——这套现代化 ArkTS 分层框架能在不改任何 H5 的前提下,优雅、高效地承载 TSGame 全部业务,并为后续能力补齐与多渠道演进留足空间。
---
## 附录 AArkTS / ArkUI 实现适配清单
> 前文为"设计级"。本附录把设计落到 ArkTS/ArkUI 的**现实工程约束**上,照此即可从"设计就绪"过渡到"编码就绪"。
### A.1 ArkUI `@Component struct` 与桥/控制器的协作
- **持有方式**`WebviewController``BridgeController` 作为 struct 的**普通成员变量**持有即可(非响应式,不要加 `@State`;它们的变化不应驱动 UI 重渲染)。需要驱动 UI 的状态(加载进度、loading 显隐)才用 `@State`/`@Local`
- **注入时机**`onControllerAttached` 回调里才能安全调用 Controller 相关接口;**能力注册(`provider.register`)放这里**。`aboutToAppear` 只做无 Controller 依赖的设置(如 debug 开关)。
- **生命周期解绑**`aboutToDisappear` 中停止能力(`provider.onDestroy`)、清空 `responseCallbacks`/`registry`、解绑 Controller,防泄漏(对应 Android `onDestroy` 的 WebView 清理)。
- **前后台**:在 `UIAbility.onForeground/onBackground` 或页面 `onPageShow/onPageHide``provider.onForeground/onBackground``callHandler('appservice','1'|'2')`
### A.2 ArkTS 严格语法注意
- 全量类型标注,**禁用 `any`**DTO 用 `interface`handler 名用 `enum`/`const``contracts` HAR)。
- 闭包/箭头函数可用,但**跨线程(TaskPool)的函数须 `@Concurrent` 且仅收发 Sendable**(见 §9.1)。
- 动态对象访问受限:解析 H5 传入 JSON 后,**显式映射到 DTO**,不要依赖结构化鸭子类型。
- 单例/DI:用模块级实例或轻量 `DIContainer`;避免在 struct 内散落 `new` 业务对象。
### A.3 线程与并发(呼应 §5.5 / §9.1)
| 场景 | 线程要求 | 手段 |
|---|---|---|
| `runJavaScript` 下发出站消息 | **必须 UI 线程** | `BridgeController.dispatch` 内置 UI 线程守卫 |
| 能力异步回调(定位/支付/传感器/广播) | 任意线程 → 切 UI | `emitter`/UIContext 任务回 UI 线程再 `callHandler` |
| 下载/解压/MD5/解密 | 子线程 | `TaskPool @Concurrent`,仅传 Sendable,进度经 `emitter` |
| 大文件 IO | 子线程 | `@ohos.file.fs` 异步 API |
### A.4 关键系统能力对照(编码备查)
| 用途 | 模块 |
|---|---|
| Web 组件/控制器 | `@kit.ArkWeb``webview.WebviewController``Web` |
| 偏好存储(urlpath 等) | `@ohos.data.preferences` |
| 文件 IO | `@ohos.file.fs` |
| 解压 | `@ohos.zlib``decompressFile` |
| XMLversion.xml | `@ohos.xml``XmlPullParser` |
| 网络请求/下载 | `@ohos.net.http``@kit.RemoteCommunicationKit``rcp` |
| 本机端口服务(截图上传) | `@ohos.net.socket`(TCP) 自建 / NAPI 原生 server |
| 并发 | `@ohos.taskpool` / `Worker` |
| 事件 | `@ohos.events.emitter` |
| 定位 | `@ohos.geoLocationManager` |
| 电量/WiFi/网络 | `@ohos.batteryInfo` / `@ohos.wifiManager` / `@kit.NetworkKit` |
| 设备/电话 | `@ohos.deviceInfo` / `@kit.TelephonyKit` |
| 扫码/相机 | Scan Kit / Camera Picker |
| 震动/传感器(摇一摇) | `@ohos.vibrator` / `@ohos.sensor` |
| 剪贴板 | `@ohos.pasteboard` |
| 音频 | `@kit.MediaKit``AVPlayer`/`SoundPool` |
> 注:具体 API 名以目标 SDK 版本为准,编码前用 `devecocli docs` 核对签名。
---
## 附录 B:生产加固与合规清单
| 项 | 要求 |
|---|---|
| **远程调试** | `setWebDebuggingAccess(true)` **仅 debug 包**`BuildProfile.DEBUG` 守卫);release 必关 |
| **密钥** | 微信 `AppSecret`/支付 `API_KEY` **不入客户端**;登录走 code、支付签名走服务端(《契约规范》§13) |
| **明文 HTTP** | 远程配置/资源走 http 需在 `module.json5` 网络安全配置中显式放行域名;逐步迁 https |
| **file:// 跨域** | 用官方方案(自定义协议/`onInterceptRequest`)而非放开全局,最小授权(§7.3) |
| **自定义 scheme 注册** | `gamepaywelcome` 等外部唤起在 `module.json5``abilities.skills.uris` 声明 |
| **权限合规** | 定位/相机/麦克风/电话**用时申请 + 文案说明**;高德等 SDK 隐私合规初始化(`updatePrivacyShow/Agree`)先于使用 |
| **渲染兜底** | `onRenderExited` 重载页面,防子进程崩溃白屏 |
| **数据安全** | KV/文件按需加密;日志脱敏(不打 openid/token 明文) |
| **上架审核** | 准备隐私声明、权限用途清单,符合 HarmonyOS 应用市场审核要求 |