# TSGame HarmonyOS 应用框架设计与开发指南 > **配套文档**:本设计严格落地《[TSGame_原生与H5接口契约总规范](./TSGame_原生与H5接口契约总规范.md)》(下称《契约规范》)。**契约规范定义"对外必须一致的行为",本文定义"HarmonyOS 侧如何优雅、高效地实现它"。** 二者配合即可让现有 H5 **零改动**运行。 > > **设计目标**:现代化(ArkTS / ArkUI 声明式 / Stage 模型)、高性能(Web 保活、并发解压下载、零主线程阻塞)、架构优雅(分层 + 依赖倒置 + 能力插件化)、专业成熟(清晰边界、可测试、可观测、可演进)。 > > **目标平台**:HarmonyOS NEXT(API 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-验收标准) - [附录 A:ArkTS / 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)`(默认开) | 访问本地文件系统 | | setAllowUniversalAccessFromFileURLs(file:// 跨域) | **无直接等价属性**;用官方跨域方案:`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 ``` > **为何这样切**:`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 Mapvoid>(); 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/', // 再次被 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// —— // 既处理 _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 首页用 `