用户明确:大厅 Web 常驻、子游戏 Web 进入创建/返回销毁(不保活);同时只一个激活(联网+收发包+界面更新), 非激活端 onInactive 彻底停网络+渲染;仅 大厅↔子游戏、无子游戏→子游戏;两 Web 各自独立桥+能力。 - 框架 §7.1:子游戏切换加"双 Web 激活模型"铁律块;M2/M4 单 WebView loadUrl 标为过渡实现 - 框架 §9:Web 保活/预热行改为双 Web 槽 + onActive/onInactive 模型 - 01_WBS:T-M5-01/02 重写为双 Web 槽(大厅常驻保活 + 子游戏临时 + 仅一个激活 + 各自独立桥),估时上调 - 记入项目记忆 m5-dual-web-activation-model Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 KiB
TSGame HarmonyOS 应用框架设计与开发指南
配套文档:本设计严格落地《TSGame_原生与H5接口契约总规范》(下称《契约规范》)。契约规范定义"对外必须一致的行为",本文定义"HarmonyOS 侧如何优雅、高效地实现它"。 二者配合即可让现有 H5 零改动运行。
设计目标:现代化(ArkTS / ArkUI 声明式 / Stage 模型)、高性能(Web 保活、并发解压下载、零主线程阻塞)、架构优雅(分层 + 依赖倒置 + 能力插件化)、专业成熟(清晰边界、可测试、可观测、可演进)。
目标平台:HarmonyOS NEXT(API 12+,建议 API 17/23),ArkWeb(方舟 Web)组件。
目录
- 设计原则
- Android → HarmonyOS 能力映射总表
- 总体架构(分层 + 模块)
- 工程结构(模块化拆分)
- 核心一:JsBridge 桥引擎设计
- 核心二:能力插件框架(CapabilityProvider)
- 容器设计:双容器模型
- 启动编排与配置/资源子系统
- 性能架构
- 横切关注点
- 契约可追溯性矩阵
- 实施路线图(里程碑)
- 验收标准
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横切层用 TypeScriptinterface/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 同名同义。
// 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 名都落到默认空实现而不抛错(对齐 AndroidBridgeWebView.defaultHandler)。这是"H5 调用永不报错"的最后防线,也是暂缓能力(§6.5)的安全网底座。
5.5 线程模型(P0,必须遵守)⚠️
Android 原版
BridgeWebView.dispatchMessage明确要求只有在主线程才下发消息(源码Thread.currentThread()==Looper.getMainLooper()才loadUrl)。HarmonyOS 同理:runJavaScript只能在 UI(主)线程调用。而能力回调(定位locationChange、支付 SDK 回调、传感器、各类系统广播、TaskPool 完成)经常发生在非 UI 线程,若直接callHandler→runJavaScript会抛异常或行为异常。
强制规则:
- 所有出站下发(
dispatch/runJavaScript)必须在 UI 线程执行。BridgeController内部对dispatch做线程守卫:非 UI 线程则投递到 UI 线程任务队列。 - 入站分发(
flushMessageQueue触发的 handler 回调)默认就在 UI 线程(onLoadIntercept在 UI 线程回调),handler 内若开了子线程做重活,回主线程再callback/callHandler。 - 能力 Provider 的异步事件回传统一经
bridge.callHandler,由桥内部保证线程切换,Provider 不需自己关心线程。
// 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 与未来替换内核:
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_bridgeHAR 不 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 插件接口
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)
// 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 的样板(以定位为例)
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还可能永久挂起。
两道安全网(缺一不可):
- 桥默认 handler 兜底:
BridgeController必须设置一个DefaultHandler(空实现),任何未注册的 handler 名都路由到它,不抛错(对齐 AndroidBridgeWebView.defaultHandler)。这是最后防线。 - 显式占位 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 兜底 | —— |
// 占位 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。基于桥协议。
@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% 时下发(与 AndroidonProgressChanged==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 直调。
@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的orientationhandler:data === '1'→ 横屏,否则 → 竖屏(对齐 AndroidsetRequestedOrientation);- 子游戏
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 零改动。
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→channel→market→game 后层覆盖(§4.4),含agentlist/gamelist两棵树与二级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_download(?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。落地范式:
// 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 |
两棵树 + 二级 url + 后层覆盖 |
| §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 行为一致(名称/参数/数据结构/时机)。
- 通用网页容器
settings6 方法 + 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 全部业务,并为后续能力补齐与多渠道演进留足空间。
附录 A:ArkTS / 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,防泄漏(对应 AndroidonDestroy的 WebView 清理)。 - 前后台:在
UIAbility.onForeground/onBackground或页面onPageShow/onPageHide调provider.onForeground/onBackground并callHandler('appservice','1'|'2')。
A.2 ArkTS 严格语法注意
- 全量类型标注,禁用
any;DTO 用interface,handler 名用enum/const(contractsHAR)。 - 闭包/箭头函数可用,但跨线程(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) |
| XML(version.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 应用市场审核要求 |