first commit

This commit is contained in:
2026-06-25 06:25:30 +08:00
commit 561af47817
46 changed files with 2719 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
/node_modules
/oh_modules
/local.properties
/.idea
**/build
/.hvigor
.cxx
/.clangd
/.clang-format
/.clang-tidy
**/.test
/.appanalyzer
+16
View File
@@ -0,0 +1,16 @@
{
"mcpServers": {
"deveco-mcp": {
"type": "stdio",
"command": "devecocli",
"args": [
"serve",
"mcp"
],
"env": {
"PROJECT_PATH": "G:\\Works\\YouleApp\\harmonyos\\gamelobby"
},
"enabled": true
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"app": {
"bundleName": "com.example.gamelobby",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"buildVersion": "1",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}
@@ -0,0 +1,8 @@
{
"string": [
{
"name": "app_name",
"value": "GameLobby"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,7 @@
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}
+93
View File
@@ -0,0 +1,93 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
HarmonyOS NEXTAPI 12+,目标 SDK `6.1.1(24)`runtimeOS `HarmonyOS`)上的 **TSGame 游戏大厅原生外壳**。目标:用 ArkTS / ArkUI 声明式 / Stage 模型重写原 Android WebView 外壳,使现有 H5(大厅 + 全部子游戏 + 活动/收银台等网页)**一行不改**即可运行。
当前处于 **M0 脚手架阶段**`entry/` 仍是 DevEco 默认模板(`Index.ets` 为 Hello World),尚未落地设计文档中的多模块架构。实现工作以 `docs/设计文档/` 为唯一依据推进。
**原 Android 项目位于 `../tsgame_android`**(相对本仓库根目录)——即本项目要复刻的 WebView 外壳源头。遇到契约/行为存疑时,以该 Android 工程的实际实现为准对照。
## 工作约定
- **🔴 H5 零改动是最高铁律**:H5 网页(大厅 + 全部子游戏)必须**完美适配新项目,不做任何修改**。所有适配工作只能发生在原生侧——H5 向原生注册的接口、原生向 H5 注册的接口、双向接口的调用时机/参数/数据结构,全部以《契约规范》为准、与原 Android 行为完全一致。**任何"改一下 H5 就好了"的方案都不被接受**;H5 行为存疑时改原生实现去迁就 H5,而非反过来。这是判断一切技术方案是否成立的首要判据。
- **原生内部实现自由**:在不违背上述铁律的前提下,原生侧的内部实现(分层、模块拆分、数据结构、设计模式、用何种 HarmonyOS API/SDK、性能优化手段等)可以**根据实际情况自由设计**,不必拘泥于原 Android 的内部写法或设计文档的伪代码细节。铁律只约束"对 H5 暴露的边界行为",边界以内怎么实现由开发者按 HarmonyOS 最佳实践决定。
- **及时自动提交 git**:每完成一个可独立验收的小步骤(一个 Provider、一个状态机节点、一处可编译通过的改动)后,**主动执行 git 提交**,无需等待用户要求。提交信息用中文、说明"做了什么 + 对应契约/设计条目(如 §8、T-M1-04)"。⚠️ 仓库当前尚未 `git init`,首次提交前需先初始化。
- HarmonyOS 相关操作(构建、运行、调试、设备、日志、查文档/SDK 签名)**必须**走 `deveco-cli` skill / `devecocli`,不要手搓 `hvigorw` 命令或直连 MCP HTTP 桥。
- **禁止直接编辑 `project.godot` 类生成文件**;本项目对应的是 **不要手改 `oh-package-lock.json5`、`build/` 产物、`.hvigor/`**——它们由工具生成。
## 常用命令
构建/运行/测试统一通过 `devecocli`(见 `deveco-cli` skill):
- 构建 HAPdebug):`devecocli build`(底层 hvigor `assembleHap`,产物 `entry/build/default/...`
- 运行到设备/模拟器:`devecocli run`;设备列表 `devecocli devices`
- 真机/模拟器日志:`devecocli log`hilog
- 查 SDK API 签名 / 鸿蒙文档:`devecocli docs <关键词>`(编码前核对 `@ohos.*` / `@kit.*` 实际签名)
测试(Hypium`@ohos/hypium` + `@ohos/hamock`):
- 本地单元测试:`entry/src/test/``LocalUnit.test.ets``List.test.ets`)——纯逻辑,不依赖设备
- 设备插桩测试:`entry/src/ohosTest/``Ability.test.ets` 等)——需真机/模拟器
- 通过 `devecocli test` 触发;目标:桥引擎 / `VersionResolver` / `MessageCodec` 单测覆盖率 **≥80%**
代码检查(`code-linter.json5`,仅作用于 `**/*.ets`):
- 规则集 `@performance/recommended` + `@typescript-eslint/recommended`
- **`@security/no-unsafe-*` 一组加密规则为 `error`**:禁用不安全的 AES/Hash/MAC/DH/DSA/ECDSA/RSA/3DES,写加密代码时务必遵守
## 架构(big picture,需读多文件才能掌握)
> 真正的架构在设计文档里,代码尚未落地。三份 SSOT 文档(修改实现前必读对应章节):
> - `docs/设计文档/TSGame_原生与H5接口契约总规范.md` —— **对外不可变契约**(H5 零改动的判据:handler 名、参数、数据结构、调用时机)
> - `docs/设计文档/TSGame_HarmonyOS框架设计与开发指南.md` —— HarmonyOS 侧如何实现(分层、桥、容器、启动)
> - `docs/设计文档/Plan/` —— 落地计划:`00_总体规划` / `01_任务分解WBS`(任务 ID `T-M{里程碑}-{序号}`/ `02_测试与验收` / `03_风险登记册`
### 分层(依赖单向向下,跨层只依赖接口)
```
① 应用/编排层 EntryAbility · StartupOrchestrator · 路由 · DI 组装根(AppModule)
② 容器层 BridgeGameContainer(大厅/子游戏) │ GenericWebContainer(通用网页)
③ 桥引擎层 BridgeController · MessageCodec · HandlerRegistry · SettingsProxy
④ 能力层 每个原生能力 = 一个 CapabilityProvider 插件(share/login/location/...)
⑤ 领域服务层 ConfigManager · ResourceManager · AppDataInjector · VersionResolver
⑥ 平台服务层 HttpClient · Downloader · Unzipper · KvStore · LocalUploadServer · TaskScheduler
横切层 Logger/Tracer · ErrorCenter · EventBus · DIContainer · Contracts(TS类型 SSOT)
```
目标多模块拆分(HAR/HSP):`entry`(①②) / `feature_bridge`(③) / `feature_capabilities`(④) / `domain_resource`(⑤) / `platform`(⑥) / `contracts`(契约类型) / `common`(横切)。
### 三条不可违背的关键约束
1. **桥核心对能力零感知**`feature_bridge` 不 import 任何具体能力,只持 `HandlerRegistry`;能力启动时把自己的 handler 注册进去。新增能力 = 新增一个 `CapabilityProvider`,零侵入桥核心。`contracts` HAR 用 `enum`/`interface` 固化所有 handler 名与 DTO,是全工程唯一来源(杜绝 `finsh`/`getcameraaAddress` 等拼写漂移)。
2. **出站下发必须在 UI 线程**(§5.5):`runJavaScript` 只能在 UI 线程调用,而能力回调常在非 UI 线程。`BridgeController.dispatch` 内置 UI 线程守卫;Provider 不关心线程,任意线程 `callHandler` 最终都在 UI 线程下发。
3. **TaskPool 只搬 Sendable**(§9.1):`@Concurrent` 任务跨线程传参/返回值必须是 Sendable**不能传** callback / `WebviewController` / `UIAbilityContext`。下载/解压进度一律经 `emitter`/`AppStorage` 回 UI 线程,不要跨线程传函数。
### 桥协议核心("H5 零改动"命门,100% 复刻 lzyzsd/JsBridge
- H5→原生:`Web().onLoadIntercept` 拦截 `yy://`(含 iframe 导航);`yy://return/` 前缀走回执,其余触发 `flushMessageQueue()`
- **队列回传不靠 `runJavaScript` 返回值**`_fetchQueue()` 在 H5 内把 `iframe.src` 置为 `yy://return/_fetchQueue/<队列JSON>`,再次经 `onLoadIntercept``handleReturnData` 路由到预登记的 `_fetchQueue` 回调。务必忠实复刻此机制。
- 原生→H5`controller.runJavaScript("WebViewJavascriptBridge._handleMessageFromNative('"+json+"');")`;桥 JS 在 `onPageEnd` 注入(直接复用库原文件)。
- **必须设 `DefaultHandler`(空实现)**:未注册的 handler 名落到默认空实现而不抛错,是"H5 调用永不报错"的最后防线。
### 双容器模型
- **BridgeGameContainer**(大厅/子游戏,对应 Android `webviewActivity`):挂桥协议;`app_data.js` 必须在**加载页面之前**由 `AppDataInjector` 写入(H5 同步 `<script src>` 读取),切勿放到 `onPageEnd``setPostUrl`/`appservice` 在首次进度 100% 时下发。子游戏切换 = 同容器 `loadUrl`,不重建组件。
- **GenericWebContainer**(通用网页,对应 `openwebActivity1`):**不挂桥**,用 `javaScriptProxy` 注入 `settings` 对象(6 方法)+ `runJavaScript` 直调;回传走结果码 101 语义经 `emitter`/路由投回上层 Bridge 容器。
### 本期范围与占位桩
本期实现分享/登录/定位/音频/摇一摇/扫码/相机/震动/剪贴板/网络/设备等 Provider。**视频房(声网)与支付用占位桩**(`RoomStubProvider`/`PayStubProvider`,§6.5):仍注册 handler,但 void 型空实现、有同步返回的回安全默认值、绝不触发出站推送——保证 H5 调用不报错、不卡死。**闲聊不涉及**(无 H5 桥接口)。组装根 `buildCapabilities()` 是唯一知道"全部能力"的地方,未来换回真实 Provider 即可。
### 两个最高风险"阻塞性验证"(必须最先证伪)
- **M1**`yy://` 的 iframe 导航是否稳定触发 `onLoadIntercept`(不同 ArkWeb 版本有差异)。失败回退:`onInterceptRequest`+`WebSchemeHandler` → 或改注入 `javaScriptProxy` 同步通道(对 H5 仍透明)。
- **M2**`file://` 加载下 H5 的 XHR/fetch 取本地资源是否可用。失败启用官方跨域方案(自定义协议 / `onInterceptRequest`)。
### 生产加固红线(附录 B
- `setWebDebuggingAccess(true)` **仅 debug 包**`BuildProfile.DEBUG` 守卫),release 必关。
- 密钥(微信 `AppSecret`、支付 `API_KEY`)**不入客户端**:登录走 code、支付签名走服务端。
- 明文 HTTP 域名需在 `module.json5` 网络安全配置显式放行;`gamepaywelcome` 等外部唤起 scheme 在 `abilities.skills.uris` 声明。
+42
View File
@@ -0,0 +1,42 @@
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}
+32
View File
@@ -0,0 +1,32 @@
{
"files": [
"**/*.ets"
],
"ignore": [
"**/src/ohosTest/**/*",
"**/src/test/**/*",
"**/src/mock/**/*",
"**/node_modules/**/*",
"**/oh_modules/**/*",
"**/build/**/*",
"**/.preview/**/*"
],
"ruleSet": [
"plugin:@performance/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@security/no-unsafe-aes": "error",
"@security/no-unsafe-hash": "error",
"@security/no-unsafe-mac": "warn",
"@security/no-unsafe-dh": "error",
"@security/no-unsafe-dsa": "error",
"@security/no-unsafe-ecdsa": "error",
"@security/no-unsafe-rsa-encrypt": "error",
"@security/no-unsafe-rsa-sign": "error",
"@security/no-unsafe-rsa-key": "error",
"@security/no-unsafe-dsa-key": "error",
"@security/no-unsafe-dh-key": "error",
"@security/no-unsafe-3des": "error"
}
}
+98
View File
@@ -0,0 +1,98 @@
# 00 · 总体规划
## 1. 里程碑总览
| 里程碑 | 主题 | 出口判据(Exit Gate) | 估时 | 可并行 |
|---|---|---|---|---|
| **M0** | 工程脚手架 + 契约类型 | 多模块工程可编译、可签名、CI 跑通;`contracts` 全量类型就位 | 3–4 人日 | — |
| **M1** | 桥引擎(最高优先) | 最小回显 H5 跑通;**`yy://` 拦截阻塞性验证通过**;桥引擎单测 ≥80% | 6–8 人日 | — |
| **M2** | 容器 + 启动 + 资源 | 真机加载现有大厅 H5、能进子游戏;**`file://` 跨域阻塞性验证通过** | 8–10 人日 | 平台层/配置/资源三轨并行 |
| **M3** | 本期能力(含占位桩) | 本期能力真机逐条对照契约通过;视频房/支付桩不报错不卡死 | 14–18 人日 | **各 Provider 高度并行** |
| **M4** | 通用网页容器 | 活动页/收银台/客服打开、`settings` 6 法 + 3 直调 + 101 回传一致 | 3–4 人日 | 可与 M3 尾段并行 |
| **M5** | 性能与加固 | 切换流畅、冷启动达标、安全审查通过、全链路可观测 | 6–8 人日 | 部分可贯穿 M3/M4 |
| **M6** | 暂缓能力集成(按需) | 真实视频房/支付打通,H5 仍零改动 | 视集成而定 | 独立 |
> 总估时(M0–M5,不含 M6):约 **40–52 人日**。按 3 人并行、关键路径串行估算,日历周期约 **5–7 周**。
## 2. 关键路径与依赖 DAG
```
M0 脚手架/契约类型
M1 桥引擎 ──(⚠ yy:// 拦截验证 = 整体可行性闸门)
M2 容器+启动+资源 ──(⚠ file:// 跨域验证 = 第二闸门)
├──────────────┬───────────────┐
▼ ▼ ▼
M3 能力(并行) M4 通用网页容器 M5 性能/加固(贯穿)
│ │ │
└──────────────┴───────────────┘
本期验收(H5 零改动)
M6 暂缓能力按需集成
```
**关键路径**`M0 → M1 → M2 → M3 → 验收`。M1/M2 的两个阻塞性验证是**最高风险节点**,必须前置(见 §5)。
## 3. 并行轨道(提升效率的关键)
| 轨道 | 启动条件 | 内容 |
|---|---|---|
| **轨道 A:桥/容器** | 立即 | M1 桥引擎 → M2 容器 → M4 通用容器 |
| **轨道 B:平台/领域** | M0 完成后即可(不必等 M1 | 平台层(Http/Download/Unzip/KvStore/Permission/TaskPool)、ConfigManager、VersionResolver、ResourceManager、StartupOrchestrator |
| **轨道 C:能力 Provider** | M1+M2 出口后 | 14+ 个 Provider **彼此独立**,可多人同时领,互不阻塞(依赖 contracts + bridge 接口已稳定) |
| **轨道 D:质量/工程** | 贯穿全程 | CI、单测、契约一致性回归、可观测、安全加固 |
> **效率要点**:轨道 B 大量纯逻辑(VersionResolver/MessageCodec/ConfigManager**不依赖真机**,可在 M1 进行时并行开发并单测;能力 Provider 在 M3 高度并行是压缩工期的核心。
## 4. 角色分工建议(按能力域,非按人头)
| 角色 | 职责 | 主要任务 |
|---|---|---|
| **桥/容器负责人** | feature_bridge、容器、ArkWeb | M1 全部、M2 容器、M4、M5 性能 |
| **平台/领域负责人** | platform、domain_resource | M2 平台/配置/资源/启动 |
| **能力工程师 ×N** | feature_capabilities | M3 各 Provider(按 SDK 熟悉度分配:分享/支付/定位/媒体…) |
| **QA/测试** | 契约一致性、真机回归 | 测试计划、回显 H5、44 handler 逐条核对 |
| **技术负责人** | 架构守门、阻塞性验证决策 | 评审两个阻塞验证结果、风险闸门 |
## 5. 阻塞性验证(必须前置,决定整体技术路线)
| 验证 | 里程碑 | 通过判据 | 不通过时的回退 |
|---|---|---|---|
| **V1`yy://` iframe 导航被 `onLoadIntercept` 捕获** | M1`T-M1-07` | 最小 H5 用 `iframe.src='yy://...'`,原生 `onLoadIntercept` 能取到该 URL 并拦截 | ① `onInterceptRequest`+`WebSchemeHandler` 注册 `yy` scheme;② 改注入的桥 JS 走 `javaScriptProxy.postMessage`(对 H5 仍透明) |
| **V2`file://` 页面 XHR/fetch 取本地资源可用** | M2`T-M2-08` | 现有大厅 H5 以 `file://` 加载,其对本地 js/css/资源的请求正常 | 用自定义协议/`onInterceptRequest` 接管本地资源响应(§7.3 框架文档) |
> **铁律**:V1 不过则 M1 不算完成,V2 不过则 M2 不算完成。两者越早证伪越好,避免后期返工。
## 6. 环境与工程约定
- **工具链**DevEco Studio + `devecocli`build/run/emulator/log/docs)。API ≥ 12(建议 17/23)。
- **工程形态**1 个 entry HAP + 7 个 HAR`feature_bridge`/`feature_capabilities`/`domain_resource`/`platform`/`contracts`/`common`)。详见框架文档 §4。
- **构建产物**`build-profile.json5``debug`/`release` 两 product`devecocli build --product <p> --build-mode <m>`
- **签名**M0 配好调试签名;release 签名走密钥库(不入库)。
- **分支策略**`main`(可发布)/ `develop`(集成)/ `feat/M{n}-{task}`(按任务)。每任务一 PR,过 CI + 代码评审合入 develop。
- **CI**:每 PR 跑 `devecocli build`debug+ 单测;develop 夜间跑真机冒烟(回显 H5 + 大厅加载)。
- **代码规范**:ArkTS 严格模式(禁 `any`)、`contracts` 为唯一 handler 名/DTO 来源、模块边界只依赖接口(框架 §3 约束)。
## 7. Definition of Done(任务级通用 DoD
一个任务"完成"须同时满足:
1. 代码合入 develop,过 CI(编译 + lint + 单测)。
2. 若涉及契约:handler 名/参数/数据结构/回传时机**逐条对照《契约规范》**无偏差。
3. 涉及线程的出站消息:经 UI 线程守卫(框架 §5.5);涉及 TaskPool:仅传 Sendable + emitter 回传(§9.1)。
4. **遵循框架附录 A(ArkTS/ArkUI 实现适配清单)**:struct 持有/生命周期解绑、严格语法、系统能力对照。
5. 有对应测试(纯逻辑→单测;能力→真机用例记录)。
6. 受影响文档 / [01 覆盖矩阵](./01_任务分解WBS.md) / [02 契约核对清单](./02_测试与验收计划.md) 更新。
## 8. 进度跟踪机制
- **任务看板**:以 `01_任务分解WBS.md` 的任务 ID 为卡片,状态 `☐/◐/☑/⚠`
- **每日站会**:同步关键路径任务与阻塞项;`⚠` 受阻项当日进风险登记册。
- **里程碑评审**:到达每个 Exit Gate 召开评审,未达判据不进入下一里程碑(M1/M2 闸门尤其严格)。
- **契约回归**M3 起,每周跑一次"44 handler 真机逐条核对",防回归。
@@ -0,0 +1,198 @@
# 01 · 任务分解(WBS
> 全量任务清单。每个任务含:**目标 / 产出物 / 依赖 / 验收 / 估时 / 角色**。
> 角色缩写:桥=桥/容器、台=平台/领域、能=能力工程师、Q=QA。估时单位:人日(pd)。
> 验收一律以《契约规范》对应条目为准;通用 DoD 见 [00_总体规划 §7](./00_总体规划.md)。
---
## M0 · 工程脚手架 + 契约类型
| ID | 目标 | 产出物 | 依赖 | 验收 | 估时 | 角色 |
|---|---|---|---|---|---|---|
| **T-M0-01** | 创建多模块工程 | `devecocli create` 生成 entry + 6 HAR`feature_bridge`/`feature_capabilities`/`domain_resource`/`platform`/`contracts`/`common` | — | 工程可 `devecocli build`debug)通过 | 1 | 桥 |
| **T-M0-02** | 模块依赖与导出 | 各 `oh-package.json5` 依赖关系(单向向下);`index.ets` 导出 | T-M0-01 | 跨模块 import 无环、可编译 | 0.5 | 桥 |
| **T-M0-03** | 构建/签名/CI | `build-profile.json5`debug/release product);调试签名;CI 脚本(build+单测) | T-M0-01 | PR 触发 CI 通过 | 1 | 桥 |
| **T-M0-04** | 契约类型(SSOT | `contracts` HAR`enum Handlers`(全部入站/出站 handler 名常量,含 `finsh`/`getcameraaAddress`/`backgameData-` 等原样拼写);DTO interface`sharetypeBean`/`MaplocationInfo`/`phoneInfoBean`/`savephotoURLBean`/`videoinfobean`/`Othervideoinfo`/`GamePay`/`sharelogin`/`sharesuccess`);`Result<T>`/`Errors`;每 handler 的 `payloadType:'raw'|'json'` 标注 | T-M0-02 | 全量对照《契约规范》§8/§9/§12,零缺漏;编译通过 | 1.5 | 台 |
| **T-M0-05** | 公共设施 | `common` HAR`Logger`(分级)/`BridgeTracer`(traceId)/`EventBus`(emitter 封装)/`DIContainer`/**`ErrorCenter`(异常收敛+弹窗/静默/上报策略)**/`Result<T>` 配套 | T-M0-02 | 单测覆盖;可被各模块引用(框架 §10) | 1.5 | 台 |
| **T-M0-06** | 引入桥 JS | 把 lzyzsd `WebViewJavascriptBridge.js` **原文件**放入 `feature_bridge/assets`,建读取/内联机制 | T-M0-01 | 文件就位、可被注入流程读取 | 0.5 | 桥 |
| **T-M0-07** | App 入口与路由壳 | `EntryAbility`Stage 模型生命周期,含 `onForeground/onBackground` 钩子接口)+ `Navigation`/路由骨架(Splash↔BridgeGameContainer↔GenericWebContainer 跳转与回传通道)+ `SplashPage` 壳(框架 §4) | T-M0-01 | 空壳可启动、可路由跳转;前后台钩子可触发 | 1 | 桥 |
---
## M1 · 桥引擎(最高优先 ⭐)
| ID | 目标 | 产出物 | 依赖 | 验收 | 估时 | 角色 |
|---|---|---|---|---|---|---|
| **T-M1-01** | Web 控制器抽象 | `IWebController` 接口 + `WebviewControllerAdapter`(包 `webview.WebviewController`+ `FakeWebController`(测试用) | T-M0-01 | 桥不直接依赖系统类;Fake 可记录脚本/模拟回执 | 1 | 桥 |
| **T-M1-02** | 消息编解码 | `Message`5 字段)+ `MessageCodec``toJson`/`toArray`/`escape`,**与 lzyzsd 完全一致的二次转义**(`\``"` | T-M0-04 | 单测:与 Android 端样例字节级一致 | 1 | 桥 |
| **T-M1-03** | Handler 注册表 | `HandlerRegistry`put/get/default+ `DefaultHandler`(空实现兜底) | T-M0-04 | 未注册名落默认 handler 不抛错(框架 §5.2/§6.5 | 0.5 | 桥 |
| **T-M1-04** | 桥控制器 | `BridgeController``onLoadIntercept`/`callHandler`/`registerHandler`/`onPageEnd`/`flushMessageQueue`/`handleReturnData`/`dispatch`/`queue`**`_fetchQueue``yy://return/` 回传**(非 runJavaScript 返回值);**UI 线程守卫**(§5.5 | T-M1-01/02/03 | 单测(FakeWebController):模拟 H5 callHandler→handler 分发、原生 callHandler→下发、回执路由、启动队列补发 | 2.5 | 桥 |
| **T-M1-05** | 桥 JS 注入 | `onPageEnd``runJavaScript(BRIDGE_JS)` + 启动消息补发 | T-M1-04/T-M0-06 | 页面就绪后 `window.WebViewJavascriptBridge` 可用 | 0.5 | 桥 |
| **T-M1-06** | 最小回显 H5 | 测试用 H5(`registerHandler('echo')``callHandler('getTime')`)+ 一个挂桥的最小测试页 | T-M1-04 | 真机:H5↔原生双向回显成功 | 1 | 桥/Q |
| **T-M1-07 ⚠** | **阻塞性验证 V1** | 真机验证 `yy://` 的 iframe 导航被 `onLoadIntercept` 捕获;产出结论报告 | T-M1-06 | V1 通过;若不通过,按 [00 §5](./00_总体规划.md) 回退方案并更新设计 | 1 | 桥/技术负责人 |
| **T-M1-08** | 桥引擎单测 | `BridgeController`/`MessageCodec`/`HandlerRegistry` 单测套件 | T-M1-04 | 覆盖率 ≥80%(框架 §13 | 1.5 | 桥/Q |
> **M1 出口**T-M1-06 跑通 + **T-M1-07V1)通过** + T-M1-08 达标。
---
## M2 · 容器 + 启动 + 资源(轨道 A 容器 / 轨道 B 平台领域)
### 平台层(轨道 B,可与 M1 并行起步)
| ID | 目标 | 产出物 | 依赖 | 验收 | 估时 | 角色 |
|---|---|---|---|---|---|---|
| **T-M2-01** | 存储/文件 | `KvStore`(@ohos.data.preferences)、`FileSystem`(@ohos.file.fs) | T-M0-01 | 读写 `urlpath`/`upurlpath`;文件增删查 | 1 | 台 |
| **T-M2-02** | 网络/下载 | `HttpClient`(@ohos.net.http 或 rcp)、`Downloader`(断点可选、进度回调) | T-M0-01 | GET 禁缓存;大文件下载带进度 | 1.5 | 台 |
| **T-M2-03** | 解压/并发 | `Unzipper`(@ohos.zlib decompressFile)、`TaskScheduler`(TaskPool/Worker) **仅传 Sendable + emitter 进度**(§9.1 | T-M0-01 | zip 解压成功;解压在子线程、UI 不阻塞 | 1.5 | 台 |
| **T-M2-04** | 权限/上传服务 | `PermissionGuard`(存储/电话/定位/相机/麦克风 用时申请);`LocalUploadServer`socket/NAPI 本机端口服务,截图上传用) | T-M0-01 | 权限流程合规;本机端口可访问 | 1.5 | 台 |
### 配置/资源/启动(轨道 B
| ID | 目标 | 产出物 | 依赖 | 验收 | 估时 | 角色 |
|---|---|---|---|---|---|---|
| **T-M2-05** | 本地+远程配置 | `ConfigManager`:内置 `rawfile/app_config.json`(§4.2 全键);远程配置请求(`http://gameconfig 去-换/ .txt?a=ts`,禁缓存);`showmessage` 阻断、<30 错误文案 | T-M2-02 | 配置键齐全;远程 JSON 正确解析、阻断逻辑生效 | 2 | 台 |
| **T-M2-06** | 分层覆盖 | `VersionResolver``agentlist`/`gamelist` 两棵树 + 二级 `url` 二次请求 + agent→channel→market→game **后层覆盖**(纯函数) | T-M2-05 | 单测:多层样例覆盖结果正确(《契约规范》§4.4) | 2 | 台 |
| **T-M2-07** | 资源管理 | `ResourceManager`:路径规范、内置包拷贝+解压、`version.xml` 解析(@ohos.xml)、远程 zip 下载/删旧/解压、版本比较 | T-M2-03/06 | 首启拷贝、版本更新下载解压到正确目录(§5) | 2 | 台 |
| **T-M2-08 ⚠** | **阻塞性验证 V2** | 真机验证 `file://` 大厅页 XHR/fetch 取本地资源;结论报告 | T-M2-07 + T-M2-10 | V2 通过;否则启用 §7.3 自定义协议方案并更新设计 | 1 | 台/桥 |
| **T-M2-09** | app_data 注入 | `AppDataInjector`:加载前写 `<urlpath>/gamehall/app_data.js`(§6 全局变量同名同义) | T-M2-07 | H5 加载时能读到 `app_*` 变量;**时序在加载前**(§7.1 铁律) | 1 | 台 |
| **T-M2-11** | 启动状态机 | `StartupOrchestrator`INIT→…→ENTER_HALL;可重试/降级/阻断 | T-M2-05/07/09 | 真机按序走通;失败降级本地缓存 | 1.5 | 台 |
### 容器(轨道 A
| ID | 目标 | 产出物 | 依赖 | 验收 | 估时 | 角色 |
|---|---|---|---|---|---|---|
| **T-M2-13** | **能力插件框架骨架** | `feature_capabilities``CapabilityProvider` 接口、`CapabilityContext`、注册表对接桥、组装根 `buildCapabilities()`/`AppModule`DI 装配)、Provider 样板(框架 §6.1/§6.2 | T-M1-04、T-M0-05 | 一个示例 Provider 可注册并被桥分发;组装根可批量装配 | 1 | 桥 |
| **T-M2-10** | 大厅/子游戏容器 | `BridgeGameContainer``Web` + 属性(JS/DOM/file/mixed/cache/geo/zoom+ 生命周期接桥(`onControllerAttached` 注册能力、`onLoadIntercept` 接桥、`onPageEnd` 注桥)+ **首次进度 100% 推 `appservice`/`setPostUrl`** + `SwitchOverGameData` 同容器 `loadUrl` 切换 + `aboutToDisappear` 清理 | T-M1-08、T-M2-13、T-M0-07 | 入口 URL 规则正确(`weburl` 空→file://,非空→http://,带 `?Launchtype=0` | 2 | 桥 |
| **T-M2-12** | 集成联调 | 大厅 H5 真机加载 + 进子游戏 | T-M2-08/10/11 | 现有大厅 `index.html` 正常显示、可进子游戏 | 1 | 桥/Q |
> **M2 出口**T-M2-12 跑通 + **T-M2-08V2)通过**。
> **注**`T-M2-13` 能力插件框架骨架是 **M3 全部 Provider 与 T-M2-10 容器注册能力的前置依赖**,须先于铺开能力完成。
---
## M3 · 本期能力(轨道 C,各 Provider 高度并行)
> **前置依赖**M3 全部 Provider 均依赖 `T-M2-13`(能力插件框架骨架)+ `T-M2-10`(容器可加载)+ `T-M0-04`(契约类型)。这三者就绪后,各 Provider 之间**互不依赖、可多人并行**。
> 通用验收(每个 Provider):入站 handler 名/参数解析、出站 handler 名/data 结构/时机**逐条对照《契约规范》§8/§9/§12**;异步回传经 UI 线程守卫;真机录用例。
| ID | Provider | 入站 handler | 出站 handler | 关键实现/SDK | 估时 | 角色 |
|---|---|---|---|---|---|---|
| **T-M3-01** | `AppSystemProvider` | `orientation``browser``finsh``openApplyDownloadpath``notification`(空) | — | 屏幕方向、拉起浏览器、退出、按包名启动/下载兜底 | 1.5 | 能 |
| **T-M3-02** | `DeviceProvider` | `getTime``getphonestate``getbattery``getwifiLevel``getcompareCode``getphoneInfo``getmarketname``getothername``getOther` | `getBattery``getwifiLevel``phonestate` | deviceInfo/telephony/batteryInfo/wifiManager;裸串 vs JSON 严格区分 | 2.5 | 能 |
| **T-M3-03** | `NetworkProvider` | `getnetwork` | `getnetwork`(广播变化) | NetworkKit 监听 | 1 | 能 |
| **T-M3-04** | `VibrateProvider` | `vibrator``repeatvibrator``canclevibrator` | — | @ohos.vibrator | 0.5 | 能 |
| **T-M3-05** | `ClipboardProvider` | `gameCopytext``gamepastetext` | —(gamepastetext 同步返回串) | @ohos.pasteboard | 0.5 | 能 |
| **T-M3-06** | `NavProvider` | `OpenurlTitleData``SwitchOverGameData``getGameinstall`(同步 0/1)、`backgameData` | `getWebdata` | 打开通用容器(M4)、同容器切换、目录判断 | 1.5 | 能/桥 |
| **T-M3-07** | `PhotoProvider` | `getphoto` | `getphoto`(下载完成数组) | 批量下载图片到本地 + Downloader | 1.5 | 能 |
| **T-M3-08** | `ShakeProvider` | `startshake``SwitchShake``stopshake` | `shakeEnd`(空串,触发后~1s) | @ohos.sensor 加速度 + 节流;声音可选 | 1.5 | 能 |
| **T-M3-09** | `LocationProvider` | `startlocation`(1连续/否单次)、`getlocationinfo`(同步) | `getlocationinfo`(MaplocationInfo/错误码) | geoLocationManager**隐私合规初始化**;错误码 0/-1/-2 | 2 | 能 |
| **T-M3-10** | `AudioProvider` | `prepareaudio`(录音)、`mediaTypeAudio``srcIsloop``voicePlaying` | `getaudiourl`(`{audiourl,time}`)、`gameui_play_voice``gameui_stop_voice` | AVPlayer/SoundPool;录音上传(七牛等)wav 路径拼接 | 3 | 能 |
| **T-M3-11** | `ScanProvider` | `opensaoma` | `getsaomaData` | Scan Kit | 1 | 能 |
| **T-M3-12** | `CameraProvider` | `opencamera` | `getcameraaAddress` | Camera Picker | 1 | 能 |
| **T-M3-13** | `ShareProvider` | `friendsSharetypeUrlToptitleDescript` | `sharesuccess`(仅微信) | 微信分享 SDK`type==2` 截图:`runJavaScript(canvas.toDataURL)` + LocalUploadServer | 2.5 | 能 |
| **T-M3-14** | `LoginProvider` | `accreditlogin` | `sharelogin`(用户资料) | 微信登录;**建议 code 模式**(密钥下沉) | 2 | 能 |
| **T-M3-15** | `RoomStubProvider`/`PayStubProvider` | `createRoom`/`exitRoom`/`getVideoinfo`/`DragViewvideoIsshow``paybrowser`/`getGameplay` | —(不触发) | **占位桩**:空实现 + 日志(§6.5 | 0.5 | 能 |
| **T-M3-16** | 前后台联动 | — | `appservice`('1'/'2') | `UIAbility.onForeground/onBackground` → callHandler | 0.5 | 桥 |
| **T-M3-17** | 契约一致性回归 | 全 44 入站 + 全出站真机逐条核对 | — | 对照《契约规范》§8/§9,出回归报告 | 2 | Q |
> **M3 出口**:本期能力真机逐条通过;**视频房/支付桩被调用不报错不卡死**(T-M3-17 含此项)。
---
## M4 · 通用网页容器(轨道 A,可与 M3 尾段并行)
| ID | 目标 | 产出物 | 依赖 | 验收 | 估时 | 角色 |
|---|---|---|---|---|---|---|
| **T-M4-01** | 通用容器 | `GenericWebContainer``Web` + cacheMode(有网 Default/无网 Only) + `javaScriptProxy` 注入 `settings` | T-M2-10 | 打开任意 url、属性正确 | 1.5 | 桥 |
| **T-M4-02** | settings 注入 + 直调 | `SettingsProxy` 6 方法(`backgameData`/`loadurl`/`browser`/`finishweb`/`isexitdialogeshow`/`isbackfinishweb`+ 3 个 `runJavaScript` 直调(`getWebdata`/`gamebackkeydown`/`backgameData` | T-M4-01 | 逐一对照《契约规范》§11.3 | 1.5 | 桥 |
| **T-M4-03** | 回传与返回键 | 101 结果经 router/emitter 回上层 → 上层 `callHandler('getWebdata')`;返回键逻辑(isbackfinishweb/isexitdialogeshow | T-M4-02 | 活动页/收银台/客服打开、回传、返回正常 | 1 | 桥 |
---
## M5 · 性能与加固(轨道 D,部分贯穿 M3/M4)
| ID | 目标 | 产出物 | 依赖 | 验收 | 估时 | 角色 |
|---|---|---|---|---|---|---|
| **T-M5-01** | Web 保活/预热 | `NodeController`+`BuilderNode` 离屏预创建/保活;启动期预热空 Web | T-M2-10 | 子游戏切换 <300ms、无白屏 | 2 | 桥 |
| **T-M5-02** | 切换与桥重绑 | 子游戏切换走 `loadUrl` 复用;切换后桥/handler 重绑时序正确 | T-M5-01 | 切换后桥功能正常 | 1 | 桥 |
| **T-M5-03** | 高频节流/批处理 | 定位/电量等高频 handler 节流;出站批处理 | T-M3-* | 跨引擎调用次数下降、无卡顿 | 1 | 桥 |
| **T-M5-04** | 崩溃兜底/内存 | `onRenderExited` 重载;`aboutToDisappear` 彻底清理 | T-M2-10 | 渲染子进程崩溃可自恢复 | 1 | 桥 |
| **T-M5-05** | 安全加固 | WebDebug 按 `isDebug` 守卫;密钥下沉服务端;明文 HTTP 域名白名单;file 跨域最小授权;自定义 scheme 注册(module.json5);日志脱敏 | 各能力就绪 | 安全审查清单(附录 B)全过 | 2 | 桥/台 |
| **T-M5-06** | 可观测 | `BridgeTracer` 全链路 traceId;接 APM/Crash | T-M0-05 | 能还原一条桥消息全链路 | 1 | 台 |
| **T-M5-07** | 冷启动优化 | StartupOrchestrator 无依赖步骤并行化 | T-M2-11 | 冷启动到首帧达标(<2s 基线) | 1 | 台 |
| **T-M5-08** | 包体/渠道裁剪(可选) | 能力 Provider 按渠道用 HSP 动态特性裁剪;包体瘦身(框架 §9 包体) | T-M3-* | 不同渠道包仅含所需能力,包体下降 | 1 | 桥/台 |
---
## M6 · 暂缓能力集成(按需,可延后)
| ID | 目标 | 产出物 | 依赖 | 验收 | 估时 | 角色 |
|---|---|---|---|---|---|---|
| **T-M6-01** | 视频房真实 | `RoomProvider`(声网 RTC HarmonyOS SDK)替换桩 | M3 完成 | `createRoom/exitRoom/getVideoinfo/DragViewvideoIsshow` + 出站 `getVideoinfo` 真打通;H5 零改动 | 视 SDK | 能 |
| **T-M6-02** | 支付真实 | `PayProvider`(浏览器收银台/或 App 内支付)替换桩;如启用 App 内支付则补 `PayuserPaytypePaystate` | M3 完成 | 支付闭环;H5 零改动 | 视 SDK | 能 |
| **T-M6-03** | 补齐项(可选) | 抖音/快手登录(code)、主动截图 `getCanvasBase64`、QQ/抖音分享回传统一 | 按需 | 对照框架 §6.4 | 视需求 | 能 |
---
## 任务总数与估时汇总
| 里程碑 | 任务数 | 估时(pd) |
|---|---|---|
| M0 | 7 | 7 |
| M1 | 8 | 9 |
| M2 | 13 | 19 |
| M3 | 17 | 24.5 |
| M4 | 3 | 4 |
| M5 | 8 | 10 |
| **合计(M0M5)** | **56** | **≈73.5 pd(含测试/缓冲)** |
| M6 | 3 | 视集成 |
> 估时为"工作量"非"日历"。3 人并行、关键路径串行下,日历周期约 5–7 周(见 [00 §1](./00_总体规划.md))。
---
## 附:框架设计 → 计划覆盖矩阵(完整性自检)
> 逐条证明《框架设计与开发指南》每个章节都有计划任务承接,**无遗漏**。
| 框架章节 | 计划任务 | 状态 |
|---|---|---|
| §1 设计原则 | 贯穿 DoD([00 §7](./00_总体规划.md))与代码规范(00 §6) | ✓ 原则类,非任务 |
| §2 能力映射总表 | 各能力/桥任务的实现依据(引用表) | ✓ 参考 |
| §3 分层架构 / §4 工程结构 | T-M0-01/02(建 entry+6 HAR)、**T-M0-07**EntryAbility/路由/Splash | ✓ |
| §5.15.2 桥协议/控制器 | T-M1-02/03/04/05 | ✓ |
| §5.3 同步/异步 | T-M1-04 + 各 Provider | ✓ |
| §5.4 裸串vsJSON | T-M0-04payloadType | ✓ |
| §5.5 线程模型 | T-M1-04UI 守卫) | ✓ |
| §5.6 IWebController | T-M1-01 | ✓ |
| §6.1/6.2 插件框架/组装根/DI | **T-M2-13**(能力插件框架骨架) | ✓ 已补 |
| §6.3 Provider 样板/截图分享 | T-M3-09(样板)、T-M3-13(分享截图) | ✓ |
| §6.4 补齐项 | T-M6-03 | ✓ |
| §6.5 占位桩 | T-M3-15 | ✓ |
| §7.1 大厅/子游戏容器 | T-M2-10 | ✓ |
| §7.2 通用网页容器 | T-M4-01/02/03 | ✓ |
| §7.3 file:// 跨域 | T-M2-08V2 阻塞验证) | ✓ |
| §8.1 启动状态机 | T-M2-11 | ✓ |
| §8.2 ConfigManager | T-M2-05 | ✓ |
| §8.2 分层覆盖 | T-M2-06VersionResolver | ✓ |
| §8.3 ResourceManager | T-M2-07 | ✓ |
| §8.4 AppDataInjector | T-M2-09 | ✓ |
| §9 性能(保活/并发/批处理/就绪渲染/内存/冷启动) | T-M5-01/02/03/04/07、T-M2-03 | ✓ |
| §9 包体/HSP 裁剪 | **T-M5-08** | ✓ 已补 |
| §9.1 TaskPool Sendable | T-M2-03 | ✓ |
| §10 契约类型 SSOT | T-M0-04 | ✓ |
| §10 日志/追踪 | T-M0-05Tracer)、T-M5-06 | ✓ |
| §10 ErrorCenter | **T-M0-05**(已含 ErrorCenter | ✓ 已补 |
| §10 事件总线/DI | T-M0-05 | ✓ |
| §10 权限 | T-M2-04PermissionGuard | ✓ |
| §10 安全 | T-M5-05 | ✓ |
| §10 可测试 | T-M1-01/08 + 各单测 | ✓ |
| §11 可追溯矩阵 | [02 测试与验收 §4](./02_测试与验收计划.md) 契约核对清单 + 本矩阵 | ✓ |
| §12 路线图 M0M6 | 本 WBS 全部里程碑 | ✓ |
| §13 验收标准 | [02 测试与验收计划](./02_测试与验收计划.md) | ✓ |
| 附录 A ArkTS/ArkUI 适配 | 贯穿 DoD[00 §7](./00_总体规划.md) 已引用 §5.5/§9.1+ T-M0-07/T-M2-10 落地 | ✓ |
| 附录 B 生产加固合规 | T-M5-05(逐项)+ [02 §5 M5 Gate](./02_测试与验收计划.md) | ✓ |
> **结论**:框架设计指南全部章节均有计划任务承接,覆盖完整。本次自检新增 `T-M0-07`、`T-M2-13`、`T-M5-08` 及 `T-M0-05` 的 ErrorCenter,补齐了能力插件框架骨架、入口/路由壳、ErrorCenter、包体裁剪四处缺口。
@@ -0,0 +1,88 @@
# 02 · 测试与验收计划
> 唯一判据:**现有 H5 零改动运行**,且行为与《契约规范》§8/§9/§11 完全一致。
## 1. 测试层次
| 层次 | 对象 | 手段 | 何时 |
|---|---|---|---|
| **单元测试** | 纯逻辑:`MessageCodec``HandlerRegistry``VersionResolver``ConfigManager` 解析、各 DTO 序列化 | ArkTS 单测 + `FakeWebController` | 随开发,CI 强制 |
| **桥协议测试** | `BridgeController` 全路径 | FakeWebController 模拟 H5 收发 | M1 |
| **集成测试** | 容器 + 启动 + 资源 + 配置 | 真机/模拟器 | M2 起 |
| **契约一致性测试** | 全 44 入站 + 全出站 handler | 真机 + 测试 H5 探针 | M3 起每周回归 |
| **端到端(E2E** | 真实大厅 H5 + 子游戏 + 活动页 | 真机手测 + 录屏 | M3/M4 |
| **性能测试** | 冷启动、子游戏切换、内存 | DevEco Profiler / hilog 打点 | M5 |
| **安全测试** | 加固清单 | 人工审查 + 工具 | M5 |
## 2. 关键测试资产
### 2.1 最小回显 H5M1 必备)
一个独立测试页,仅用桥 API
- `registerHandler('echo', (d,cb)=>cb(d))``callHandler('getTime')``callHandler('echo','hi')`
- 验证:H5→原生→回执、原生→H5→回执、启动前积压消息补发、`yy://` 拦截(V1)。
### 2.2 契约探针 H5M3 回归)
一个覆盖**全部 44 入站 handler**的探针页:逐个 `callHandler` 并记录回传,与《契约规范》期望值比对,输出"通过/差异"报告。出站 handler 用 `registerHandler` 全量挂载、记录原生推送。
> 此页是"H5 零改动"的自动化守门员,纳入每周回归。
## 3. 两个阻塞性验证(最高优先,决定技术路线)
| 验证 | 用例 | 通过判据 | 不通过 → 动作 |
|---|---|---|---|
| **V1 `yy://` 拦截**M1, T-M1-07 | 最小 H5 设 `iframe.src='yy://__queue__'`,原生 `onLoadIntercept` 取 URL | 能拿到 `yy://` URL 并拦截、`_fetchQueue` 回执经 `yy://return/` 到达 | 切 `onInterceptRequest`+`WebSchemeHandler`;或改桥 JS 走 `javaScriptProxy.postMessage`[00 §5](./00_总体规划.md) |
| **V2 `file://` 跨域**M2, T-M2-08 | 现有大厅 `index.html``file://` 加载,请求本地 js/css/图片/`app_data.js` | 资源全部正常加载、H5 正常运行 | 启用自定义协议/`onInterceptRequest` 接管本地资源(框架 §7.3 |
> 这两项必须在对应里程碑**作为 Exit Gate 强制通过**。
## 4. 契约一致性核对清单(H5 零改动判据)
### 4.1 桥协议(§2
- [ ] `window.WebViewJavascriptBridge` 注入后 API 与事件 `WebViewJavascriptBridgeReady` 一致
- [ ] `registerHandler`/`callHandler` 双向、回执、`callbackId`/`responseId` 路由正确
- [ ] `_fetchQueue``yy://return/` 回传(非 runJavaScript 返回值)
- [ ] 启动前积压消息在注桥后补发
### 4.2 入站 44 handler(§8)—— 逐条核对名称/参数/同步返回
- [ ] 账号社交:`accreditlogin``friendsSharetypeUrlToptitleDescript``getphoto`
- [ ] 支付:`paybrowser``getGameplay`(**本期桩,验"不报错不卡死"**
- [ ] 设备信息:`getTime``getphonestate``getbattery``getwifiLevel``getnetwork``getcompareCode``getphoneInfo``getmarketname``getothername``getOther`
- [ ] 交互:`orientation``vibrator``repeatvibrator``canclevibrator``gameCopytext``gamepastetext``notification`(空)
- [ ] 摇一摇:`startshake``SwitchShake``stopshake`
- [ ] 定位:`startlocation``getlocationinfo`
- [ ] 音频:`prepareaudio``mediaTypeAudio``srcIsloop``voicePlaying`
- [ ] 扫码/相机/浏览器/网页:`opensaoma``opencamera``browser``OpenurlTitleData``openApplyDownloadpath`
- [ ] 子游戏/房间:`SwitchOverGameData``getGameinstall``createRoom``exitRoom``getVideoinfo``DragViewvideoIsshow`**房间本期桩**
- [ ] 退出/返回:`finsh``backgameData`
### 4.3 出站 handler(§9)—— 逐条核对名称/data 结构/时机
- [ ] `appservice`(1/2)、`setPostUrl``getWebdata``getphoto``sharelogin``sharesuccess``shakeEnd``getBattery``getwifiLevel``getnetwork``getaudiourl`(+`filepath`)、`gameui_play_voice``gameui_stop_voice``getphoneinfo``getVideoinfo`(桩不推)、`PayuserPaytypePaystate`(桩不推)、`getlocationinfo``phonestate``getsaomaData``getcameraaAddress`
- [ ] 名称陷阱:`backgameData-`(带连字符)≠ 入站 `backgameData`
### 4.4 数据结构(§12
- [ ] `sharetypeBean`/`MaplocationInfo`/`phoneInfoBean`/`savephotoURLBean`/`videoinfobean`/`Othervideoinfo`/支付/`sharelogin`/`sharesuccess` 字段名同名同义
### 4.5 启动/资源/容器(§3–§7、§11)
- [ ] 配置键齐全、远程配置分层覆盖、`showmessage` 阻断
- [ ] 资源路径、version.xml 版本比较、zip 下载解压
- [ ] `app_data.js` 全局变量、**加载前注入**
- [ ] 入口 URL`weburl` 空/非空)、`?Launchtype=` 参数
- [ ] 通用网页容器 `settings` 6 法 + 3 直调 + 101 回传
## 5. 里程碑验收 Gate
| 里程碑 | Gate |
|---|---|
| M0 | 工程编译/签名/CI 通过;`contracts` 全量类型对照契约零缺漏 |
| **M1** | 回显 H5 双向通;**V1 通过**;桥单测 ≥80% |
| **M2** | 真机加载大厅 H5、进子游戏;**V2 通过**app_data.js 时序正确 |
| M3 | §4.2/§4.3/§4.4 清单全过;**桩能力不报错不卡死** |
| M4 | §11.3 通用容器一致;活动页/收银台/客服打开回传正常 |
| M5 | 切换 <300ms、冷启动 <2s(基线);安全清单(附录 B)全过;可观测可用 |
| **本期总验收** | 现有大厅 + 全部子游戏 + 网页页面**零改动运行**;§4 全清单通过 |
## 6. 缺陷分级与回归
- **P0**:H5 无法运行/桥不通/崩溃 → 当日修复,阻塞合入。
- **P1**:某 handler 行为不符契约 → 当迭代修复。
- **P2**:性能/体验 → M5 收口。
- 每次合入触发"契约探针 H5"回归,防止已通过的 handler 退化。
@@ -0,0 +1,47 @@
# 03 · 风险登记册
> 概率/影响:高/中/低。状态:开放/缓解中/关闭。受阻任务(`⚠`)须在此登记。
## 1. 技术风险(最高优先)
| ID | 风险 | 概率 | 影响 | 触发信号 | 缓解 / 应对 | 负责人 |
|---|---|---|---|---|---|---|
| **R-01** | **`yy://` 自定义 scheme 的 iframe 导航不被 `onLoadIntercept` 捕获**(桥不通) | 中 | 高(阻断全局) | T-M1-07 验证失败 | 前置到 M1 最先验证;回退:`onInterceptRequest`+`WebSchemeHandler` 注册 `yy` scheme;或改桥 JS 走 `javaScriptProxy.postMessage`(对 H5 透明)。三方案均零改 H5 | 桥/技术负责人 |
| **R-02** | **`file://` 页面跨域受限**H5 的 XHR/fetch 取本地资源失败 | 中 | 高(大厅/子游戏白屏) | T-M2-08 验证失败 | 前置到 M2;回退:自定义协议/`onInterceptRequest` 接管本地资源(框架 §7.3)。必要时把首页改用自定义 scheme 加载 | 台/桥 |
| **R-03** | `MessageCodec` 转义与 lzyzsd 不一致,含特殊字符的 data 解析错位 | 中 | 中 | 含引号/反斜杠/中文的分享文案、JSON data 解析异常 | 单测以 Android 端样例做字节级对照;复用库 JS 原文件 | 桥 |
| **R-04** | **线程问题**:能力回调在非 UI 线程调 `runJavaScript` 崩溃/丢消息 | 中 | 高 | 定位/支付/传感器回调偶发崩溃 | 强制 UI 线程守卫(框架 §5.5);能力回传统一经桥(emitter/UIContext | 桥 |
| **R-05** | **TaskPool Sendable 约束**导致下载/解压回调传参编译/运行失败 | 中 | 中 | `@Concurrent` 报 Sendable 错 | 仅传 Sendable + emitter 回传(§9.1);路径在 UI 线程取好传字符串 | 台 |
| **R-06** | 远程配置二级 `url`/分层覆盖逻辑与 Android 不一致,选错下载地址/版本 | 中 | 中 | 资源版本判断错误、下错包 | `VersionResolver` 纯函数 + 多层样例单测,对照《契约规范》§4.4 | 台 |
| **R-07** | Web 保活(NodeController/BuilderNode)生命周期/内存复杂,切换异常或泄漏 | 中 | 中 | 切换白屏/内存涨 | M5 才上;先用 `loadUrl` 切换保底;保活作为优化项可回退 | 桥 |
| **R-08** | `app_data.js` 注入时序错误(加载后才写),H5 读不到配置 | 低 | 高 | 大厅读 `app_*` 为 undefined | 铁律:`INJECT_APP_DATA` 早于进入容器(框架 §7.1/§8.4);测试覆盖 | 台 |
## 2. 第三方 SDK / 平台风险
| ID | 风险 | 概率 | 影响 | 缓解 / 应对 | 负责人 |
|---|---|---|---|---|---|
| **R-09** | 微信/分享/定位等厂商 **HarmonyOS SDK 缺失或能力不全** | 中 | 中 | 早期做 SDK 可用性盘点;缺失则系统能力替代(如系统分享/系统定位)或服务端代理;登录/支付走 code/服务端 | 能 |
| **R-10** | 声网/支付 SDK 集成成本高 | 中 | 低(本期桩规避) | 本期占位桩,M6 按需集成;不阻塞主线 | 能 |
| **R-11** | 高德等定位 SDK **隐私合规**未先行,定位失败/审核不过 | 中 | 中 | 使用前 `updatePrivacyShow/Agree`;权限用时申请 + 文案;优先系统定位 | 能 |
| **R-12** | 本机 HTTP 上传服务(截图)实现复杂(无内置 server) | 中 | 低 | `@ohos.net.socket` 自建轻量服务或 NAPI;截图分享非核心可后置 | 台 |
| **R-13** | ArkWeb 版本差异导致 API 行为变化 | 低 | 中 | 锁定目标 API 版本;编码前 `devecocli docs` 核对签名;CI 固定 SDK | 桥 |
## 3. 工程 / 进度风险
| ID | 风险 | 概率 | 影响 | 缓解 / 应对 | 负责人 |
|---|---|---|---|---|---|
| **R-14** | 团队 ArkTS/ArkUI 经验不足,前期效率低 | 中 | 中 | M0/M1 配对开发;沉淀附录 A 适配清单;统一脚手架样板 | 技术负责人 |
| **R-15** | 契约理解偏差导致 handler 行为不符,后期返工 | 中 | 中 | `contracts` HAR 固化为 SSOT;契约探针 H5 每周回归;评审对照 | Q |
| **R-16** | 能力并行开发接口/风格不统一 | 低 | 低 | `CapabilityProvider` 统一接口 + Provider 样板;代码评审 | 桥 |
| **R-17** | 真机/设备不足影响联调与回归 | 低 | 中 | 早期备真机 + 模拟器;CI 夜间冒烟 | PM |
## 4. 受阻任务记录(动态更新)
| 日期 | 受阻任务 | 关联风险 | 描述 | 处置 | 状态 |
|---|---|---|---|---|---|
| — | — | — | (执行期间填写) | — | — |
## 5. 风险闸门(与里程碑绑定)
- **M1 闸门**:R-01 必须收敛(V1 通过或回退方案落地)方可进入 M2。
- **M2 闸门**:R-02 必须收敛(V2 通过或回退方案落地)方可大规模铺 M3 能力。
- **M5 闸门**R-04/R-07 收敛 + 安全清单(框架附录 B)通过方可发布。
+32
View File
@@ -0,0 +1,32 @@
# TSGame HarmonyOS 开发计划(Plan
> 本目录是《[TSGame_HarmonyOS框架设计与开发指南](../TSGame_HarmonyOS框架设计与开发指南.md)》的**落地实施计划**,把"设计"拆解为"可执行、可验收、可追踪"的任务。
>
> 上游依据(单一事实来源 SSOT):
> - 《[TSGame_原生与H5接口契约总规范](../TSGame_原生与H5接口契约总规范.md)》—— 对外不可变契约(H5 零改动的判据)
> - 《[TSGame_HarmonyOS框架设计与开发指南](../TSGame_HarmonyOS框架设计与开发指南.md)》—— HarmonyOS 侧如何实现
## 文档导航
| 文件 | 内容 | 读者 |
|---|---|---|
| [00_总体规划.md](./00_总体规划.md) | 里程碑、关键路径、依赖 DAG、并行轨道、角色分工、环境/分支、DoD、进度机制 | 全员 / PM |
| [01_任务分解WBS.md](./01_任务分解WBS.md) | **核心**:按里程碑的全量任务(ID/目标/产出/依赖/验收/估时/角色) | 开发 |
| [02_测试与验收计划.md](./02_测试与验收计划.md) | 测试策略、回显 H5、两个阻塞性验证、契约一致性回归、里程碑验收 Gate | 开发 / QA |
| [03_风险登记册.md](./03_风险登记册.md) | 风险、概率/影响、缓解、触发条件、负责人 | PM / 技术负责人 |
## 一页速览(TL;DR
- **目标**:在 HarmonyOS 上实现原生外壳,使现有 H5(大厅 + 全部子游戏 + 活动/收银台等网页)**一行不改**即可运行。
- **本期范围**:桥引擎、双容器、启动/配置/资源、本期全部设备能力。**视频房、支付用占位桩**(不报错/不卡死),**闲聊不涉及**。真实视频房/支付列入 M6 按需集成。
- **最高优先 & 最大风险**:M1 桥引擎,以及两个"阻塞性验证"——`yy://` 拦截(M1)、`file://` 跨域(M2)。这两点必须**最先证伪**,决定整体可行性。
- **关键路径**M0 → M1 → M2 →(M3 能力并行)→ M4 → M5。M6 可延后。
- **判据**:以《契约规范》§8/§9/§11 的 handler 名、参数、数据结构、调用时机为唯一验收标准。
## 任务 ID 规范
`T-M{里程碑}-{序号}`,例:`T-M1-04` = 里程碑 M1 的第 4 个任务。跨文档引用任务用此 ID。
## 状态约定
`☐ 未开始` · `◐ 进行中` · `☑ 完成` · `⚠ 受阻`(受阻须在 [03_风险登记册](./03_风险登记册.md) 记录)。
@@ -0,0 +1,762 @@
# 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`):同容器内 `controller.loadUrl()` 到目标目录,桥与 handler 不变。
### 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:// 跨域
H5 以 `file://` 加载并请求本地/远程资源。HarmonyOS 侧采用官方"Web 页面跨域解决方案":用 `onInterceptRequest` 自定义本地资源响应,或为本地页配置自定义协议/响应头,避免 file 同源限制。**对 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 保活 / 预热** | 用 `NodeController` + `BuilderNode` 离屏预创建并保活 Web 组件;大厅↔子游戏切换走 `loadUrl` 而非重建组件;可在启动期预热一个空 Web 实例 | 子游戏进入<300ms,避免内核重启白屏 |
| **并发卸载重活** | 下载/解压/解密/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` | 两棵树 + 二级 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 行为一致(名称/参数/数据结构/时机)。
- [ ] 通用网页容器 `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 应用市场审核要求 |
@@ -0,0 +1,845 @@
# TSGame 原生 ↔ H5 接口契约与启动流程总规范(跨平台适配版)
> **文档目的**:本规范以现有 Android 工程的**真实代码**为唯一依据,抽象出"原生容器"与"H5 游戏内容"之间的**完整接口契约**和**启动/资源加载流程**。
> 目标是:在一个全新的原生平台(HarmonyOS)上重新实现"原生容器",使**现有 H5 一行代码都不改动**即可正常运行。
>
> **适配原则**
> - 本文档只规定"契约"(协议、接口名、参数结构、调用时机、数据流),**不规定原生内部如何实现**——原生侧(包括 HarmonyOS)可根据平台能力自由实现,只要对外行为与本契约一致即可。
> - 文中所有接口名、参数字段、常量值均来自真实源码,并标注了来源文件与行号,便于核对。
> - 凡现有 Android 代码中"未实现/已注释/无回传"的部分,本文明确标注,作为新平台需要**补齐或决策**的事项。
---
## 目录
1. [总体架构](#1-总体架构)
2. [核心:JS 通信桥协议(WebViewJavascriptBridge](#2-核心js-通信桥协议webviewjavascriptbridge)
3. [应用启动流程](#3-应用启动流程)
4. [配置体系](#4-配置体系)
5. [资源(大厅/子游戏)管理](#5-资源大厅子游戏管理)
6. [H5 数据注入:app_data.js](#6-h5-数据注入app_datajs)
7. [WebView 容器能力要求](#7-webview-容器能力要求)
8. [接口契约一:H5 → 原生(入站 Handler 全集)](#8-接口契约一h5--原生入站-handler-全集)
9. [接口契约二:原生 → H5(出站 Handler 全集)](#9-接口契约二原生--h5出站-handler-全集)
10. [原生能力专题](#10-原生能力专题)
11. [子游戏跳转与通用网页容器](#11-子游戏跳转与通用网页容器)
12. [数据结构汇总](#12-数据结构汇总)
13. [关键常量与第三方账号](#13-关键常量与第三方账号)
14. [HarmonyOS 适配检查清单与待决策项](#14-harmonyos-适配检查清单与待决策项)
---
## 1. 总体架构
整个 App 本质上是一个**多 WebView 容器 + 资源管理器**,所有业务逻辑(大厅、棋牌子游戏)都是 H5,原生只提供"启动引导 + 资源下载解压 + 设备能力桥接"。
```
┌──────────────────────────────────────────────────────────────┐
│ 原生容器(可在任意平台重写) │
│ │
│ [启动引导页] ──► 读本地配置 ──► 请求远程配置 ──► 下载/解压资源 │
│ │ │ │
│ └──────────────► 注入 app_data.js ─────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ WebView 容器(大厅 / 子游戏) │ │
│ │ 加载 file://.../index.html?Launchtype=X │ │
│ │ │ │
│ │ ◄──── JS 通信桥(WebViewJavascriptBridge ────► │ │
│ │ H5 ⇄ 原生:登录/分享/支付/定位/音频/震动… │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ 设备能力:微信/QQ/抖音分享、微信支付登录、高德定位、 │
│ 录音、音效、摇一摇、扫码、相机、剪贴板、震动… │
└──────────────────────────────────────────────────────────────┘
```
### 1.1 容器角色划分(现有 Android 实现)
| 角色 | 现有 Android 类(仅作来源标注,新平台无需同名) | 说明 |
|---|---|---|
| 启动引导页 | `weclomeactivity1`(Launcher,强制横屏) | 读配置、请求远程配置、下载解压资源、注入 `app_data.js`,完成后跳大厅 |
| 大厅容器 | `webviewActivity` | 加载大厅 H5`.../gamehall/index.html` |
| 子游戏/内置网页容器 | `openwebActivity1` | 由大厅通过桥接口 `OpenurlTitleData` 打开任意 H5 url |
| 微信回调容器 | `NewwebviewActivity` | 与大厅容器接口集几乎完全相同,承担微信 scheme 回调 |
> **重要说明**`webviewActivity` 与 `NewwebviewActivity` 注册的桥接口(44 个入站 handler)**名称、参数结构、回调名几乎完全一致**,差异仅 3 处(见 §8.x 标注)。新平台**只需实现一套统一的桥接口契约**即可同时覆盖大厅与子游戏容器。本文档给出的是这套统一契约。
---
## 2. 核心:JS 通信桥协议(WebViewJavascriptBridge
> 这是整个适配的**重中之重**。H5 完全依赖此协议与原生通信,**不可更改**。新平台必须 1:1 实现此协议,包括其 URL scheme、消息格式、JS 全局对象与注入时机。
> 来源:`com/tagmae/jsbridge/``BridgeWebView`、`BridgeWebViewClient`、`BridgeUtil`、`Message`)。本协议是开源库 lzyzsd/JsBridge 的实现。
### 2.1 协议总览
> ⚠️ **重要范围说明**:本章描述的 `WebViewJavascriptBridge` 协议适用于**大厅容器与子游戏容器**(基于 `BridgeWebView`,即 `webviewActivity` / `NewwebviewActivity`)。
> **另有一个独立的通用网页容器 `openwebActivity1`**(由桥接口 `OpenurlTitleData` 打开,用于活动页/收银台/客服等"打开网页"场景),它**不使用本协议**,而是用传统的"**JS 接口对象注入(`@JavascriptInterface`,对象名 `settings`+ `javascript:` 直接函数调用**"。该容器的接口契约见 [§11.3](#113-通用网页容器-openwebactivity1-的独立接口契约),新平台**两套机制都要实现**。
在基于 `BridgeWebView` 的两个主容器里,业务通信**不使用**平台的"JS 接口注入"机制(如 Android `@JavascriptInterface`、HarmonyOS `javaScriptProxy`)作为通道——其中所有 `addJavascriptInterface(new settings(), "settings")` 均已注释失效,仅保留的崩溃监控用途与业务无关。**这两个主容器的业务通信 100% 走下述 URL scheme 拦截协议。**
> 另:源码中存在 `ShareJavascriptInterface`(注入名 `NativeShare`,方法 `shareToQQ`/`shareToDouYin`),但其注入入口 `ShareActivityPatch` 在全工程**无任何引用,是死代码**,H5 无法依赖,新平台**无需实现**。
通信靠两条单向通道拼成双向:
1. **原生 → H5**:原生执行 JS`WebViewJavascriptBridge._handleMessageFromNative('<消息JSON>')`
2. **H5 → 原生**:H5 改变一个隐藏 iframe 的 `src``yy://...` 触发导航;原生在"资源加载拦截/URL 跳转拦截"里识别 `yy://` 前缀并阻断真实导航,转而处理消息。
### 2.2 URL scheme 约定(必须原样实现)
来源:`BridgeUtil.java`
| 常量 | 值 | 含义 |
|---|---|---|
| 协议前缀 | `yy://` | H5 发起的所有桥调用 |
| 返回数据前缀 | `yy://return/` | H5 对原生调用的回执 / 取队列回执 |
| 取消息队列 | `yy://return/_fetchQueue/` | 原生通知 H5"把待发消息队列交出来" |
原生侧 URL 拦截逻辑(必须复刻):
```
拦截到 url(需先 URLDecode):
if url 以 "yy://return/" 开头:
→ 解析出 functionName 与 data,执行对应回调,阻断导航
else if url 以 "yy://" 开头:
→ 调用 _fetchQueue()(见下),阻断导航
else:
→ 正常导航
```
### 2.3 原生 → H5 的两条 JS 调用(必须原样实现)
来源:`BridgeUtil.java`
- 下发单条消息:`javascript:WebViewJavascriptBridge._handleMessageFromNative('<JSON字符串>');`
- 抽取 H5 待发队列:`javascript:WebViewJavascriptBridge._fetchQueue();`
- 该调用本身被当作一次"原生→H5"调用,其回执由 H5 通过 `yy://return/_fetchQueue/<JSON数组>` 送回原生。
### 2.4 消息体(MessageJSON 结构
来源:`Message.java`。原生与 H5 之间所有消息都序列化为如下结构(字段按需出现):
| 字段 | 类型 | 含义 |
|---|---|---|
| `handlerName` | String | 目标 handler 名(指定调用哪个已注册的 handler;无则走 defaultHandler |
| `data` | String | 业务数据载荷(**注意:可能是裸字符串,也可能是 JSON 字符串**,逐接口而定) |
| `callbackId` | String | 本条消息期望对端回执时带回的 id(发起方生成) |
| `responseId` | String | 回执消息:对应此前收到的 `callbackId` |
| `responseData` | String | 回执消息的返回数据载荷 |
原生侧生成 callbackId 的格式:`JAVA_CB_<自增id>_<时间戳>`(来源 `BridgeWebView.doSend`)。新平台可用任意唯一字符串格式,H5 不关心其内部格式,只负责原样回带。
### 2.5 JS 端 APIH5 使用,原生必须保证这些 API 存在且行为一致)
H5 通过全局对象 `window.WebViewJavascriptBridge` 调用以下方法(由注入的 `WebViewJavascriptBridge.js` 提供):
| JS API | 作用 |
|---|---|
| `registerHandler(handlerName, function(data, responseCallback){...})` | H5 注册一个 handler,供**原生调用**(即接收"原生→H5"消息)。`responseCallback(retData)` 用于把结果回给原生。 |
| `callHandler(handlerName, data, function(responseData){...})` | H5 **调用原生**注册的 handler`responseData` 回调接收原生的同步返回。 |
| `init(function(message, responseCallback){...})` | 设置默认 handler(接收未指定 handlerName 的消息)。 |
| `send(data, responseCallback)` | 向原生默认 handler 发消息。 |
H5 监听桥就绪事件后才开始调用(典型写法,原生需保证此事件/回调时序):
```javascript
function connectBridge(cb){
if (window.WebViewJavascriptBridge) { cb(WebViewJavascriptBridge); }
else { document.addEventListener('WebViewJavascriptBridgeReady',
function(){ cb(WebViewJavascriptBridge); }, false); }
}
```
> 新平台需提供与 lzyzsd/JsBridge **同名同行为**的 `WebViewJavascriptBridge.js`,确保 H5 中已有的 `registerHandler`/`callHandler` 调用全部生效。建议直接复用该库的 `WebViewJavascriptBridge.js` 原文件。
### 2.6 JS 注入时机(关键时序)
来源:`BridgeWebViewClient.onPageFinished`
- 在**每个页面加载完成(onPageFinished)时**,原生把 `WebViewJavascriptBridge.js` 内容注入页面(作为 JS 执行)。
- 注入后,原生把"启动消息队列"(页面加载完成前积压的待发消息)逐条 `_handleMessageFromNative` 下发。
新平台实现要点:
1. 监听页面加载完成事件 → 注入桥 JS。
2. 维护"启动前积压消息队列":页面就绪前原生若调用 H5 handler,先入队,页面就绪后补发。
3. 注入的 JS 解析时会**去除以 `//` 开头的注释行**`BridgeUtil.assetFile2Str`),新平台若直接读取 js 文件注入,需保持等价(或直接内联完整 js)。
### 2.7 一次完整调用的数据流示例
**H5 调用原生(带返回)** —— 例:H5 获取系统时间
```
H5: bridge.callHandler('getTime', '', function(ret){ /* ret = "1700000000000" */ });
└─ 桥生成 message {handlerName:'getTime', data:'', callbackId:'cb_1'}
入队 → 触发 iframe.src='yy://__queue__'
原生: 拦截 yy:// → _fetchQueue() 取出该 message
→ 找到名为 'getTime' 的 handler 执行 → responseCallback("1700000000000")
→ 原生把 {responseId:'cb_1', responseData:'1700000000000'} 经
_handleMessageFromNative 下发
H5: 桥按 responseId 找到 cb_1 回调并执行
```
**原生调用 H5(推送)** —— 例:定位结果推送
```
原生: callHandler('getlocationinfo', '<MaplocationInfo的JSON>', null)
→ message {handlerName:'getlocationinfo', data:'<json>'} 经
_handleMessageFromNative 下发
H5: 桥查到 H5 用 registerHandler('getlocationinfo', ...) 注册的处理函数并执行
```
---
## 3. 应用启动流程
> 来源:`weclomeactivity1.java`、`initwebviewutil`、`GameupdateUtil.java`、`webviewActivity.initwebview`。
> 下述为**行为时序**,新平台按此顺序复刻即可,内部实现自由。
### 3.1 启动总时序
```
启动引导页 onCreate
│ 全屏、强制横屏、保持屏幕常亮
读取本地配置(资源目录名编码,见 §4.1)
│ initfile = "gamehall"gamestart 配置)
│ filestart = "FtJf...ziK"gamedir 配置)
申请运行时权限(存储 / 电话状态 / 定位)
拼接远程配置 URL(见 §4.3
│ configUrl = "http://" + (gameconfig 配置去'-'换'/') + ".txt"
首次安装?
├─ 是:把内置预置包(gamehall)拷贝到内部存储并解压
└─ 否:比较内置 version.xml 与已解压 version.xml 版本,必要时重新拷贝
检测网络 → 请求远程配置(HTTP GET,禁缓存,URL 追加 "?a=<时间戳>"
解析远程配置(JSON
├─ 2.0 格式(含 gamelist):拉取代理二级配置 + 游戏二级配置 → 分层版本计算
└─ 1.0 格式:单文件逐层匹配
版本决策:远程 game_version > 本地 version.xml 的 version
├─ 需要更新:下载 game_download 指向的 zip → 删旧目录 → 解压到资源目录
└─ 无需更新:直接用本地资源
(如有)APK 自升级:app_version 高于本地 appversion → 下载 APK 安装
注入 app_data.js(把配置写进大厅资源目录,见 §6)
跳转大厅容器(不带 Intent extra;配置经 app_data.js 与桥接口获取)
大厅容器加载 file://.../gamehall/index.html?Launchtype=0
```
### 3.2 阻断式提示
远程配置 JSON 顶层若含非空 `showmessage` 字段,启动流程会**弹窗提示并阻断**(用于公告/停服)。配置内容长度过短(< 30 字符)时视为错误文案,直接弹窗。新平台需复刻此"全局公告阻断"行为。
---
## 4. 配置体系
### 4.1 本地配置编码机制(资源目录名编码)
> 来源:`getAllFilename(String name)``weclomeactivity1` / `webviewActivity` 均有同名实现)。
现有 Android 把每个配置项做成一个**资源目录**,目录下放一个**以"配置值"命名的子文件夹**(外加一个占位文件 `BoolTest.java` 需被跳过)。读取逻辑:
```
列出 assets/<配置名>/ 下的条目
跳过名为 "BoolTest.java" 的占位文件
剩下那个"子文件夹的名字"即为配置值
返回该名字(找不到则为空字符串)
```
> **HarmonyOS 适配建议**:这种"用目录名编码配置"的做法是历史包袱。新平台**推荐改为正规的键值配置文件**(如随包内置一个 JSON),只要能提供下表中的同名配置值即可,H5 完全无感知。
### 4.2 配置项清单(本仓库实测值)
| 配置键 | 本仓库实际值 | 含义 / 用途 |
|---|---|---|
| `agent` | `veRa0qrBf0df2K1G4de2tgfmVxB2jxpv` | 代理商 ID(远程配置分层匹配 key) |
| `channel` | `FtJf073aa0d6rI1xD8J1Y42fINTm0ziK` | 渠道 ID(分层匹配 key) |
| `gamedir` | `FtJf073aa0d6rI1xD8J1Y42fINTm0ziK` | 资源解压**父目录名** |
| `gamestart` | `gamehall` | 资源解压后的**游戏目录名**(大厅入口所在目录) |
| `appversion` | `49` | 当前 App 版本号(数字,与远程 `app_version` 比较决定是否升级 APK |
| `market` | `3` | 市场 ID(分层匹配 key;也经桥 `getmarketname` 暴露给 H5 |
| `gameid` | (空,仅占位) | 游戏 ID(空时回退用 version.xml 中的 game id |
| `weburl` | (空) | 大厅 H5 远程地址;**空 → 本地 file:// 加载**(本仓库走本地) |
| `gameconfig` | `tsgames.daoqi88.cn-config_test-update_jsonv2_test` | 远程配置地址(`-``/` 后拼 `.txt` |
| `other` | (空) | 业务自定义值,经桥 `getOther`/`getothername` 暴露给 H5 |
| `tuiguang` | (目录不存在 → 空) | 推广/邀请码,写入 app_data.js 的 `app_invitationcode` |
| `servertype` | (空,**无代码读取**) | 历史预留,启动流程不使用,可不实现 |
| `gameserver` | (空,**无代码读取**) | 历史预留,启动流程不使用,可不实现 |
### 4.3 远程配置请求
**URL 拼装**(来源 `weclomeactivity1.init`):
```
gameconfig 值 = "tsgames.daoqi88.cn-config_test-update_jsonv2_test"
→ 把 '-' 替换为 '/' = "tsgames.daoqi88.cn/config_test/update_jsonv2_test"
→ configUrl = "http://" + 上一步 + ".txt"
= "http://tsgames.daoqi88.cn/config_test/update_jsonv2_test.txt"
请求时再追加防缓存参数: configUrl + "?a=" + 当前毫秒时间戳
请求方式:HTTP GET,强制不走缓存
```
**返回体**:一个 `.txt` 文件,内容为 **JSON**(解析前用 JSON 解析校验;长度 < 30 视为错误提示文案直接弹窗)。存在两套并存格式:
#### 2.0 配置格式(主路径,含 `gamelist` 字段时)
```jsonc
{
"showmessage": "", // 非空 → 弹窗阻断(公告/停服)
"agentlist": [
{
"agentid": "", "agentname": "",
"app_version": "", "app_download": "", "app_size": "",
"game_version": "", "game_download": "", "game_size": "",
"url": "", // 指向"代理专属二级配置"地址,非空则二次请求
"channellist": [ /* marketlist */ ]
}
],
"gamelist": [
{
"gameid": "", "url": "", // url 指向"游戏专属二级配置",非空则二次请求
"agentlist": [ /* channellist marketlist */ ]
}
]
}
```
解析流程:
1. 用本机 `agent` 值在 `agentlist` 命中项;若其 `url` 非空 → 二次请求拉**代理二级配置**。
2. 用本机 `gameid` 值在 `gamelist` 命中项;若其 `url` 非空 → 二次请求拉**游戏二级配置**。
3. 两个二级配置都到齐后,做最终版本决策(见 §4.4)。
#### 1.0 配置格式(兼容老格式,无 `gamelist` 时)
单文件内嵌全部层级,直接在一个循环里逐层匹配,无二次请求。
### 4.4 分层覆盖逻辑(agent → channel → market → game
> **此分层覆盖逻辑真实存在**,来源 `GameupdateUtil.agentUtil`(已核实)。匹配 key 为 `{agentid, gameid, channelid, marketid}`(来自本地配置)。
- **代理配置树**`agentid 命中` → 遍历 `channellist`(channelid 命中) → 遍历 `marketlist`(marketid 命中,可设 app 升级) → 遍历该市场 `gamelist`(gameid 命中,设 app/game 升级)。**越深层越后赋值,覆盖前层**。
- **游戏配置树**`gameid 命中``agentlist`(agentid) → `channellist`(channelid) → `marketlist`(marketid,可设 app+game 升级)。同样**后层覆盖前层**。
- **最终合并**:app 升级与 game 升级各自取"代理树与游戏树中 version 更高"者的下载地址,再分别与本地版本比较。
每层可携带的可覆盖字段:`app_version``app_download``app_size``game_version``game_download``game_size``url``showmessage`
> **与旧参考文档的差异提示**:旧版 `TSGame_应用启动流程详解.md` 中给出的配置 JSON`data.agentlist[].channellist[].marketlist[]`、`config_download`、`configVersion` 等字段)是**示意性质,与真实字段名不完全一致**。请以本节真实字段(`agentlist` / `gamelist` 两棵树并存、二级 `url` 二次请求、`game_download`/`game_version` 等)为准。
---
## 5. 资源(大厅/子游戏)管理
### 5.1 真实文件系统路径
> 来源 `initwebviewutil.getdate`。
```
内部存储根 = <App内部files目录> // 例如 Android: /data/data/<包名>/files
父目录(upurlpath) = <files>/tsgames/<包名>/<启动毫秒时间戳>
解压根(urlpath) = <父目录>/<gamedir值> // = .../<时间戳>/FtJf073aa0d6rI1xD8J1Y42fINTm0ziK
游戏内容目录 = <解压根>/<gamestart值> // = .../FtJf...ziK/gamehall
大厅入口 = <解压根>/gamehall/index.html
```
两个路径以键值对持久化(供容器读取):`urlpath`(解压根)、`upurlpath`(父目录)。
> 注意:解压根含**启动时间戳**,意味着每次全新解压会落到新目录。新平台可沿用或简化为固定目录,对 H5 无影响(H5 只看到自己被以 `file://` 加载)。
### 5.2 内置预置包(首次安装)
- 现有工程随包内置 `gamehall` 目录,首启时整体拷贝到解压根,并对其中的 `.zip` / `.so` 解压,然后删除压缩包。
- **本仓库内置包 `gamehall_42_0527.zip` 仅 553 字节,里面只有一个 `version.xml`,是占位/演示包**——真正可运行的大厅 H5(index.html 等)由远程 zip 下载提供。新平台需注意:**不能假设内置包里有完整大厅页面**。
### 5.3 版本文件 version.xml
> 资源目录与内置包内均含 `version.xml`,结构如下(实测):
```xml
<game>
<agent id="..." name="天盛网络"/>
<game id="..." name="友乐游戏"/>
<channel id="..." name="友乐互动游戏"/>
<version value="42" name="1.42"/>
</game>
```
- `version value` 为整数,用于与远程配置 `game_version` 比较(远程更高则下载更新)。
- `agent/game/channel` 的 id 作为资源自带的归属标识。
### 5.4 zip 下载与解压
```
下载 URL = 远程配置 game_download 字段值 + "?a=<时间戳>"
下载到 = <App外部files目录>/dowlod_zip/<时间戳>Projects.zip (目录名拼写即代码原样 "dowlod_zip"
解压步骤:
1. 删除旧的 <解压根>/gamehall 内容
2. 解压 zip 到 <解压根>/ zip 内顶层即含 gamehall/...
3. 删除 zip 文件
4. 进入"注入 app_data.js → 跳大厅"收尾
```
### 5.5 APK 自升级
- 远程 `app_version` 高于本地 `appversion` 时,下载 `app_download` 指向的安装包并触发系统安装。
- HarmonyOS 适配:改为对应平台的应用更新方式即可,与 H5 契约无关。
---
## 6. H5 数据注入:app_data.js
> 来源 `weclomeactivity1.inith5data`。**这是把原生配置传给 H5 的关键机制之一,H5 直接读取这些全局变量,必须复刻。**
跳转大厅前,原生在大厅资源目录生成/改写 `app_data.js`
```
路径:<解压根>/gamehall/app_data.js
```
文件内容为一组全局 JS 变量声明(H5 直接引用),字段如下:
| 全局变量 | 来源 | 含义 |
|---|---|---|
| `app_version` | version.xml 的 version | 资源版本号 |
| `app_gameconfig` | `gameconfig` 配置 | 远程配置地址标识 |
| `app_gamedir` | `gamedir` 配置 | 资源父目录名 |
| `app_gamestart` | `gamestart` 配置 | 游戏目录名(gamehall |
| `app_agent` | `agent` 配置 | 代理商 ID |
| `app_appversion` | `appversion` 配置 | App 版本号 |
| `app_market` | `market` 配置 | 市场 ID |
| `app_channel` | `channel` 配置 | 渠道 ID |
| `app_invitationcode` | `tuiguang` 配置 | 推广/邀请码 |
| `app_Launchtype` | 固定 `'0'`(大厅) | 启动类型 |
| `app_gamename` | 资源/配置 | 游戏名 |
| `app_getwifisignalLevel` | 运行时 | WiFi 信号等级初值 |
> 新平台只需在加载大厅页面前,保证大厅目录下存在等价的 `app_data.js`(同名全局变量、同含义取值)。内部如何生成文件不限。
---
## 7. WebView 容器能力要求
> 来源 `webviewActivity.initWebViewSettings` / `NewwebviewActivity.initWebViewSettings`。下列为 H5 正常运行所需的**能力要求**,新平台用等价配置满足即可。
| 能力 | 要求 | 说明 |
|---|---|---|
| JavaScript | 必须启用 | — |
| DOM Storage / 本地数据库 | 必须启用 | H5 用 localStorage 等持久化 |
| 本地文件访问 | 必须允许 | 加载 `file://` 本地页面 |
| `file://` 跨源访问 | 必须允许(universal access from file URLs | H5 从 file 页面请求本地/远程资源 |
| 混合内容(http+https | 允许(always allow | 本地页面内嵌 http 资源 |
| 明文 HTTP 流量 | 允许 | 远程配置/资源走 http |
| 缓存 | 不使用缓存(每次走网络/本地最新) | — |
| 定位 | 启用(H5 geolocation + 桥定位) | — |
| 缩放 | 关闭(禁缩放、禁内置缩放控件、禁 wide viewport | 固定布局游戏 |
| 多窗口 | 关闭 | — |
| UserAgent | **未自定义**(用平台默认) | H5 不依赖特定 UA |
| Cookie | 未特殊配置 | — |
| 远程调试 | 启用(开发期) | — |
容器还需提供:
- **本机 HTTP 服务**:用于"截图分享上传"。容器启动一个本机 HTTP server,把地址(`http://<本机IP>:<端口>/testurl`)通过桥 `setPostUrl` 推给 H5(见 §9)。新平台需提供等价的本机上传端点能力,否则截图分享链路不通。
---
## 8. 接口契约一:H5 → 原生(入站 Handler 全集)
> H5 通过 `bridge.callHandler('<名称>', '<data>', responseCallback)` 调用。
> **data 列**标注是裸字符串还是 JSON 字符串;**同步返回列**标注是否通过 `responseCallback` 同步返回(裸字符串/JSON);多数能力的结果是**异步**通过 §9 的出站 handler 推回。
> 来源:`webviewActivity.initwebjh`(行 21853012/ `NewwebviewActivity.initwebjh`(行 21643129)。两容器一致,差异在表后标注。
### 8.1 账号 / 社交
| Handler | data | 同步返回 | 异步回传 | 功能 |
|---|---|---|---|---|
| `accreditlogin` | 裸字符串(`"1"`=QQ,其他=微信;QQ 实为空实现) | 无 | `sharelogin` | 触发授权登录(实际仅微信) |
| `friendsSharetypeUrlToptitleDescript` | JSON`sharetypeBean`,见 §12 | 无 | `sharesuccess`(仅微信有结果) | 分享(微信/QQ/抖音) |
| `getphoto` | 裸字符串(图片下载 JSON 数组) | 无 | `getphoto` | 批量下载图片/头像到本地 |
### 8.2 支付
| Handler | data | 同步返回 | 异步回传 | 功能 |
|---|---|---|---|---|
| `paybrowser` | 裸字符串(收银台 URL) | 无 | 无(H5 收银台页自理) | 浏览器方式支付(**当前生效**) |
| `getGameplay` | JSON`price/body/typeplay/type/user` | 无 | `PayuserPaytypePaystate` | App 内微信支付(**仅旧版 `webviewActivity` 生效**`NewwebviewActivity` 已注释) |
### 8.3 设备 / 系统信息
| Handler | data | 同步返回 | 异步回传 | 功能 |
|---|---|---|---|---|
| `getTime` | 忽略 | 裸字符串(毫秒时间戳) | — | 获取系统时间 |
| `getphonestate` | 忽略 | 裸字符串 int(0 挂断/1 接起/2…) | — | 获取电话状态 |
| `getbattery` | 忽略 | 无 | `getBattery` | 主动获取电量 |
| `getwifiLevel` | 忽略 | 无 | `getwifiLevel` | 主动获取 WiFi 信号 |
| `getnetwork` | 忽略 | 裸字符串(`1`无网/`2`WiFi/`3`移动) | — | 主动获取网络状态 |
| `getcompareCode` | 忽略 | 裸字符串 int(1=本地版本>网络版本,0=否) | — | App 版本比较结果 |
| `getphoneInfo` | 忽略 | 无 | `getphoneinfo` | 获取手机配置信息(型号/IMEI 等) |
| `getmarketname` | 忽略 | 裸字符串(market 值) | — | 获取市场 ID |
| `getothername` | 裸字符串(配置名) | 裸字符串(该配置值) | — | 获取任意本地配置值 |
| `getOther` | 忽略 | 裸字符串(other 值) | — | 获取 other 配置值 |
### 8.4 交互 / 反馈
| Handler | data | 同步返回 | 异步回传 | 功能 |
|---|---|---|---|---|
| `orientation` | 裸字符串(`"1"`横屏,其他竖屏) | 无 | — | 切换屏幕方向 |
| `vibrator` | 裸字符串 long(毫秒) | 无 | — | 震动 |
| `repeatvibrator` | 裸字符串 int`-1`否/`1`重复) | 无 | — | 重复震动 |
| `canclevibrator` | 忽略 | 无 | — | 取消震动 |
| `gameCopytext` | 裸字符串(文本) | 无 | — | 写入剪贴板 |
| `gamepastetext` | 忽略 | 裸字符串(剪贴板内容) | — | 读取剪贴板 |
| `notification` | 裸字符串 | 无 | — | 发送通知(**空实现,未落地**) |
### 8.5 摇一摇
| Handler | data | 异步回传 | 功能 |
|---|---|---|---|
| `startshake` | 忽略 | `shakeEnd` | 开始监听摇一摇 |
| `SwitchShake` | 裸字符串(`"1"`开声音震动/其他关) | — | 摇一摇声音开关 |
| `stopshake` | 忽略 | — | 停止监听 |
### 8.6 定位
| Handler | data | 同步返回 | 异步回传 | 功能 |
|---|---|---|---|---|
| `startlocation` | 裸字符串(`"1"`连续/其他单次) | 无 | `getlocationinfo` | 开启定位 |
| `getlocationinfo` | 忽略 | JSON`MaplocationInfo`;未就绪返回错误码 JSON) | — | 主动取最近一次定位 |
### 8.7 音频
| Handler | data | 异步回传 | 功能 |
|---|---|---|---|
| `prepareaudio` | 忽略 | — | 准备/开始录音(长按) |
| `mediaTypeAudio` | JSON`audiourl/type/user` | `gameui_play_voice` / `gameui_stop_voice` | 播放语音消息 |
| `srcIsloop` | JSON`src/isloop`) | 无 | 播放游戏音效/背景乐 |
| `voicePlaying` | 裸字符串(`"1"`开/其他静音) | — | 语音播放总开关 |
| `getaudiourl` 录音结果 | (由 `prepareaudio` 触发) | `getaudiourl` | 录音上传后回传地址 |
### 8.8 扫码 / 相机 / 浏览器 / 网页
| Handler | data | 同步返回 | 异步回传 | 功能 |
|---|---|---|---|---|
| `opensaoma` | 忽略 | 无 | `getsaomaData` | 打开扫一扫 |
| `opencamera` | 忽略 | 无 | `getcameraaAddress` | 打开相机拍照 |
| `browser` | 裸字符串(URL) | 无 | — | 系统/QQ 浏览器打开链接 |
| `OpenurlTitleData` | JSON`url/title/data` | 无 | `getWebdata` | 打开内置网页容器(子游戏跳转,见 §11) |
| `openApplyDownloadpath` | JSON`packagename/downloadpath`) | 无 | — | 按包名启动 App,失败则浏览器打开下载地址 |
### 8.9 子游戏切换 / 房间(音视频)
| Handler | data | 同步返回 | 异步回传 | 功能 |
|---|---|---|---|---|
| `SwitchOverGameData` | JSON`webtype/Gamedirectory/gamedownloadurl/data`) | 无 | — | 切换子游戏(容器内切换) |
| `getGameinstall` | 裸字符串(目录名) | 裸字符串(`1`已装/`0`未装) | — | 判断子游戏资源是否已就绪 |
| `createRoom` | 裸字符串(`videoinfobean` JSON) | 无 | — | 加入/创建音视频房间(声网) |
| `exitRoom` | 裸字符串 | 无 | — | 退出房间 |
| `getVideoinfo` | 裸字符串(`Othervideoinfo` JSON | 无 | `getVideoinfo` | 请求/绑定远端视频窗口 |
| `DragViewvideoIsshow` | 裸字符串(`"1"`显示/其他隐藏) | 无 | — | 视频悬浮框显隐 |
### 8.10 退出 / 返回
| Handler | data | 异步回传 | 功能 | 备注 |
|---|---|---|---|---|
| `finsh` | 忽略 | — | 退出游戏 | 名字即代码原样(拼写如此) |
| `backgameData` | 裸字符串(data) | — | 子游戏带数据返回大厅 | **仅 `NewwebviewActivity` 有**;以结果码 101 回传给上层容器 |
| `getAddressBook` | 忽略 | `getAddressbook` 已注释) | 获取通讯录 | **未实现** |
### 8.11 两容器差异小结
| 接口 | `webviewActivity`(大厅) | `NewwebviewActivity`(子游戏/微信回调) |
|---|---|---|
| `getGameplay`(App 内微信支付) | ✅ 生效 | ❌ 已注释(改用 `paybrowser` |
| `backgameData`(带数据返回) | ❌ 无 | ✅ 有 |
| `getlocationinfo` 错误码 | 未就绪返回裸 `null` | 返回结构化错误码 JSON`-1` 未就绪/`-2` 权限拒绝) |
| 分享内部分流 | 直连微信 SDK 多分支 | 按 `sharefriend` 分流到统一分享面板 |
| 其余 ~40 个 handler | 完全一致 | 完全一致 |
> 适配结论:实现**统一一套**入站 handler,并对差异项采用更完整的一侧(如 `getlocationinfo` 用结构化错误码、同时支持 `paybrowser` 与 `backgameData`)即可兼容两个容器。
---
## 9. 接口契约二:原生 → H5(出站 Handler 全集)
> 原生通过 `callHandler('<名称>', '<data>', null)` 调用;**H5 必须用 `registerHandler('<名称>', ...)` 注册才能收到**。
> 来源:两容器内 `callHandler` 调用点。
| Handler | data | 调用时机 |
|---|---|---|
| `appservice` | 裸字符串(`"1"`前台/`"2"`后台) | 页面首次加载到 100%、以及前后台切换(resume/pause/stop、息屏/解锁) |
| `setPostUrl` | 裸字符串(`http://<本机IP>:<端口>/testurl`) | 页面加载完成时,下发截图上传地址 |
| `getWebdata` | 裸字符串(网页回传 text / Intent 透传 data) | 页面首次加载完成;或内置网页关闭回传(结果码 101) |
| `getphoto` | JSON 数组(`[{pid, photourl}]` | 图片/头像下载完成 |
| `sharelogin` | JSON`openid/headimgurl/nickname/sex/city/province/unionid` | 微信授权登录成功后 |
| `sharesuccess` | JSON`{success:int, type:int}`success 2 成功/3 取消) | 分享结果回调(仅微信完整) |
| `shakeEnd` | 空字符串 `""` | 摇一摇触发后约 1 秒 |
| `getBattery` | 裸字符串 float(0~1) | H5 主动取电量时;或电量变化广播 |
| `getwifiLevel` | JSON`{ssidname:String, signalLevel:int(0~4)}`) | H5 主动取时;或 WiFi 变化广播 |
| `getnetwork` | 裸字符串 int`1`断网/`2`WiFi/`3`移动) | 网络状态变化广播 |
| `getaudiourl` | JSON`{audiourl:String, time:float}`;旧容器 `webviewActivity` 录音成功时**额外携带 `filepath:String`**,值与 `audiourl` 相同;取消时 audiourl="" time=0 | 录音上传完成 / 录音取消 |
| `gameui_play_voice` | 裸字符串(`user` 透传) | 语音开始播放 |
| `gameui_stop_voice` | 裸字符串(`user` 透传) | 语音播放完成 / 被打断 |
| `getphoneinfo` | JSON`phoneInfoBean`,见 §12 | H5 调 `getphoneInfo` 后 |
| `getVideoinfo` | 裸字符串 int(远端 uid) | 音视频房间其他用户加入时 |
| `PayuserPaytypePaystate` | JSON`{user, state, typeplay, type, data}`state 1 成功/0 取消) | App 内微信支付结果(仅旧版容器) |
| `getlocationinfo` | JSON`MaplocationInfo`;或错误码 JSON) | 定位成功(单次 1 次/连续每 5s);或权限被拒 |
| `phonestate` | 裸字符串 int(来电状态) | 来电状态广播 |
| `getsaomaData` | 裸字符串(扫码结果) | 扫码返回 |
| `getcameraaAddress` | 裸字符串(照片路径) | 拍照返回 |
> 名称陷阱:`NewwebviewActivity` 中有一处出站调用名为 `backgameData-`**末尾带连字符**,data 为空字符串),用于返回键确认对话框流程;与入站的 `backgameData`(无连字符)是两个不同名字。适配时按代码原样区分。
---
## 10. 原生能力专题
> 本节给出各设备能力的"H5 触发 → 入参 → 回传时机/结构",原生内部如何对接第三方 SDK 不限。
### 10.1 分享(微信 / QQ / 抖音)
- **触发**`friendsSharetypeUrlToptitleDescript`,入参 `sharetypeBean`(§12)。
- `sharefriend=="2"` → 微信朋友圈直发;`=="1"` → 弹分享面板由用户选目标(微信好友/QQ/抖音)。
- `type``"1"`网页/文本,`"2"`Canvas 截图(原生实时截当前 WebView 的 canvas 转 base64),`"3"`图片链接,抖音另有 `"4"`视频。
- **回传**:仅微信完整 → `sharesuccess``{success,type}`2 成功/3 取消)。
- **现状/待补**:QQ、抖音当前**无结果回传**(QQ 回调被注释,抖音仅 Toast / 走外部 App scheme)。若 H5 依赖分享回调,新平台需为三端补齐统一 `sharesuccess` 回传。
### 10.2 微信登录
- **触发**`accreditlogin`(非 `"1"` 即微信),scope=`snsapi_userinfo`
- **回传**`sharelogin`,回传**用户资料**openid/unionid/nickname/headimgurl/sex/city/province),非 code。
- **安全提示**:现有实现用客户端硬编码 AppSecret 换 token(见 §13),新平台**建议改为回传 code、由服务端换取**。
### 10.3 支付
- **当前生效**`paybrowser`(data 为收银台 URL,原生开浏览器,无回传,H5 收银台自理)。
- **旧版 App 内支付**`getGameplay` → 微信 SDK → `PayuserPaytypePaystate` 回传结果(仅旧版容器)。`PayReq` 字段:`appId/partnerId/prepayId/packageValue("Sign=WXPay")/nonceStr/timeStamp/sign(MD5大写)`
### 10.4 抖音 / 快手登录
- **未实现**:现有 `sgapi` 实为"闲聊(XianliaoSDK"且整文件注释,无桥接入。新平台如需,须全新对接抖音开放平台/快手 SDK,建议回传 code 给 H5。
### 10.5 截图 / Canvas
- 截图当前**耦合在分享流程内部**,无独立 handler。原生注入 JS 调 H5 `canvas.toDataURL`(默认 image/jpeg 720×405;指定 id 则 png),失败回退原生绘制;结果直接交分享,不回传 H5。
- **待补建议**:若 H5 需主动截图,新增 handler(如 `getCanvasBase64`,入参 canvasId/格式/尺寸,返回纯 base64 或 dataURL)。
### 10.6 音频
- 远程语音消息:`mediaTypeAudio` 播放 → `gameui_play_voice` / `gameui_stop_voice` 回传(透传 `user`)。
- 游戏音效/背景乐:`srcIsloop``src` 为 wav 文件名(非 URL),路径 `<解压根>/<game>/assets/wav/<src>``isloop``0` 播一次 / `>0` 循环 / `<0` 停止。无回传。
- 静音总开关:`voicePlaying`
- 录音:`prepareaudio` 录音 → 上传 → `getaudiourl` 回传 `{audiourl,time}`
### 10.7 摇一摇
- `startshake` 开始 → 触发后约 1 秒 `callHandler('shakeEnd','')` → 自动重新监听。`SwitchShake` 控声音,`stopshake` 停止。
- 灵敏度等参数原生内部决定(现有实现加速度阈值约 3500)。
### 10.8 定位(高德)
- `startlocation``"1"`连续/5s 间隔,其他单次)→ `getlocationinfo``MaplocationInfo`
- `getlocationinfo` 也可同步主动取(一名两用)。
- 错误码约定:`0` 成功 / 高德原始码 / `-1` 未就绪 / `-2` 权限被拒。
- 新平台用本平台定位服务实现,**只要回传 `MaplocationInfo` 同结构同字段即可**。
---
## 11. 子游戏跳转与通用网页容器
> H5 从大厅进入"另一个页面"有**两条独立路径**,机制完全不同,新平台都要实现:
> - **路径 A(容器内切换子游戏)**`SwitchOverGameData` → 在**同一个 BridgeWebView 容器**内重载到另一个游戏目录,仍走 §2 Bridge 协议。
> - **路径 B(打开通用网页)**`OpenurlTitleData` → 打开**独立的通用网页容器 `openwebActivity1`**,该容器用 §11.3 的**另一套接口机制**`settings` 对象注入 + `javascript:` 直调)。
### 11.1 路径 A:容器内切换子游戏(SwitchOverGameData
> 来源:`webviewActivity`/`NewwebviewActivity` 的 `SwitchOverGameData` → `initgame()`。
- H5 调用:`bridge.callHandler('SwitchOverGameData', JSON.stringify({webtype, Gamedirectory, gamedownloadurl, data}))`
- `webtype``"2"`竖屏 / `"3"`横屏
- `Gamedirectory`:目标游戏目录名
- `gamedownloadurl`:游戏 id(用于判断/下载)
- `data`:交换数据
- 原生在**当前 BridgeWebView 容器内**重载到 `file://<解压根>/<Gamedirectory>/index.html?Launchtype=1`(必要时先下载解压该游戏 zip),**桥协议与全部 handler 不变**,对 H5 透明。
**关键结论**:本路径下子游戏 H5 通常已包含在 §5 下载解压的整包内;原生按 `Gamedirectory` 切换目录加载,仍复用同一套 Bridge 接口。
### 11.2 路径 B:打开通用网页(OpenurlTitleData
> 来源:`webviewActivity`/`NewwebviewActivity` 的 `OpenurlTitleData` → 打开 `openwebActivity1`。
```
大厅/游戏 H5 拼好目标地址(本地 file:// 或远程 http://
bridge.callHandler('OpenurlTitleData', JSON.stringify({url, title, data}))
原生打开通用网页容器 openwebActivity1,加载该 urlIntent extraurl/title/data/orientation
│ orientation"1"=竖屏,其他=横屏
页面加载到 100% → 原生用【javascript: 直调】getWebdata('<data>') 把交互数据交给页面
页面内 H5 调 settings.backgameData('<data>')(或返回键确认)→ 原生 setResult(101, data) 退出
回到上层 Bridge 容器 → 上层收到出站 getWebdata(携带回传 data
```
> 注意:`openwebActivity1` **不是 BridgeWebView**,页面里**无 `window.WebViewJavascriptBridge`**H5 在该容器内只能用 §11.3 的 `settings` 接口和被原生 `javascript:` 直调的全局函数。
### 11.3 通用网页容器 openwebActivity1 的独立接口契约
> 来源:`openwebActivity1.java`。机制 = **普通 WebView + 注入对象 `settings``@JavascriptInterface`+ 原生 `javascript:` 直接调用全局函数**。新平台需为该容器单独实现这套契约(等价于 HarmonyOS 的 `javaScriptProxy` + `runJavaScript`)。
#### (1) H5 → 原生:全局注入对象 `settings`(对象名固定为 `settings`
H5 直接调用 `window.settings.<方法>(...)`
| 方法签名 | 参数 | 功能 |
|---|---|---|
| `settings.backgameData(data)` | `data`:String | 带数据返回上层容器(原生 `setResult(101,{data})` 并关闭本页) |
| `settings.loadurl(urls)` | `urls`:String | 在本容器加载新 url |
| `settings.browser(browserurl)` | `browserurl`:String | 用系统/QQ 浏览器打开链接 |
| `settings.finishweb()` | 无 | 直接关闭本页 |
| `settings.isexitdialogeshow()` | 无 | 开启"返回时弹退出确认框"行为 |
| `settings.isbackfinishweb()` | 无 | 设置返回键改为交给 H5(触发下方 `gamebackkeydown()`)而非直接关闭 |
#### (2) 原生 → H5`javascript:` 直接调用的全局函数(H5 需在 window 上定义同名函数)
| 全局函数(H5 须实现) | 参数 | 调用时机 |
|---|---|---|
| `getWebdata('<data>')` | 字符串 data(来自打开时传入的 `data`) | 页面首次加载到 100% 时 |
| `gamebackkeydown()` | 无 | 按下返回键、且 H5 已通过 `settings.isbackfinishweb()` 接管返回键、且 WebView 可后退时 |
| `backgameData()` | 无(**注意:与 H5→原生的 `settings.backgameData(data)` 同名但无参、方向相反**) | "返回退出确认框"点确认时,原生回调通知页面即将退出 |
#### (3) 该容器的 WebView 设置差异(相对 §7 主容器)
- **不注入** `WebViewJavascriptBridge.js`;无 `yy://` 拦截。
- 缓存策略不同:有网时用默认缓存(`LOAD_DEFAULT`),无网时用缓存优先(`LOAD_CACHE_ELSE_NETWORK`);启用 AppCache。
- 同样启用:JavaScript、DOMStorage、文件访问、file:// 跨源访问、定位;禁缩放/禁多窗口。
- WebViewClient 自行拦截 `weixin:` / `alipayqr:` / `alipays:` / `tel:` 等 scheme 跳转到系统处理。
> 适配要点:新平台需提供**两类容器**——
> ① BridgeWebView 容器(大厅+子游戏,§2 协议 + §8/§9 全部 handler);
> ② 通用网页容器(§11.3 的 `settings` 注入对象 6 方法 + `getWebdata`/`gamebackkeydown`/`backgameData` 三个 `javascript:` 直调)。
> 两类容器都实现到位,现有 H5(含其打开的活动页/收银台/客服等)才能**零改动**运行。
---
## 12. 数据结构汇总
> 字段名严格对应代码,新平台序列化必须保持**同名同语义**。
### sharetypeBean(分享入参,全 String
```jsonc
{
"sharefriend": "1", // "1" 好友(弹面板) / "2" 朋友圈(直发微信)
"type": "1", // "1" 网页/文本 / "2" Canvas截图 / "3" 图片链接 / "4" 视频(抖音)
"sharetype": "", // 分享子类型
"webpageUrl": "https://...", // 分享链接 或 图片地址
"title": "标题",
"description": "描述"
}
```
### MaplocationInfo(定位回传)
```jsonc
{
"latitude": 0.0, "longitude": 0.0, // double
"address": "", "country": "", "province": "", "city": "",
"district": "", "street": "", "cityCode": "", "streetNum": "",
"adCode": "", "aoiName": "", // 以上 String
"accuracy": 0.0, // float
"locationType": 0, // int
"errorCode": 0, // int0 成功 / -1 未就绪 / -2 权限拒绝 / 其他=底层错误码
"errorMsg": "" // String
}
```
### phoneInfoBean(手机信息回传,全 String
```jsonc
{
"PhoneVersion": "", "PhoneAdresseMAC": "", "PhoneModel": "",
"PhoneDeviceBrand": "", "PhoneProvidersName": "",
"PhoneIMEI": "", "PhoneIMSI": ""
}
```
### savephotoURLBean(图片下载,getphoto 回传数组元素)
```jsonc
{ "pid": "", "photourl": "" }
```
### videoinfobeancreateRoom 入参,全 String
```jsonc
{ "playerid":"", "roomid":"", "agentid":"", "gameid":"",
"left":"", "top":"", "pmw":"", "pmh":"", "width":"", "height":"" }
```
### OthervideoinfogetVideoinfo 入参)
```jsonc
{ "playerid":"", "left":"", "top":"", "width":"", "height":"", "pmw":"", "pmh":"" }
```
### 支付相关
- `getGameplay` 入参:`{ "price":"", "body":"", "typeplay":int, "type":"", "user":int }`
- `PayuserPaytypePaystate` 回传:`{ "user":"", "state":int(1成功/0取消), "typeplay":"", "type":"", "data":"" }`
### sharelogin(微信登录回传,全 String
```jsonc
{ "openid":"", "headimgurl":"", "nickname":"", "sex":"", "city":"", "province":"", "unionid":"" }
```
### sharesuccess(分享回传)
```jsonc
{ "success": 2, "type": 1 } // success: 2成功/3取消;type: 1好友/2朋友圈
```
---
## 13. 关键常量与第三方账号
> 来源:`simcpux/Constants.java`、`AndroidManifest.xml`。新平台需用自己的应用账号重新申请,下列仅说明依赖项。
| 项 | 值 | 说明 |
|---|---|---|
| 微信 AppID | `wxd2bd650e06bdfe58` | 分享/登录/支付 |
| 微信商户号 MCH_ID | `1448669802` | App 内支付 |
| 微信 API_KEY | `ClMrQsAcidRa7uJT4TgHgVAOHbzQjdPa` | 支付签名(**应下沉服务端**) |
| 微信 AppSecret | `1934a281c82ad1a059130fe51341b74b` | 登录换 token**应下沉服务端**) |
| 高德定位 API Key | `4f92bacc16cace69a6045a544d6e3c7d` | 定位 |
| 微信回调 scheme | `wxd2bd650e06bdfe58` | 第三方回调 |
| 自定义 scheme | `gamepaywelcome``gamepaywxd2bd650e06bdfe58` | H5/外部唤起原生页 |
**关键权限**:存储读写、读电话状态、定位(精确+粗略)、安装应用、网络、相机、录音、允许明文 HTTP。
> ⚠️ 安全提示:现有工程把微信 AppSecret / API_KEY 明文硬编码在客户端,存在风险。HarmonyOS 新版应将签名与换 token 逻辑下沉到服务端,登录改为回传 code。
---
## 14. HarmonyOS 适配检查清单与待决策项
### 14.1 必做(保证 H5 零改动运行)
- [ ] **实现 WebViewJavascriptBridge 协议**URL scheme `yy://` 拦截、`_handleMessageFromNative` / `_fetchQueue`、Message JSON 结构、`onPageFinished` 注入 `WebViewJavascriptBridge.js`、启动消息队列补发(§2)。直接复用该库的 `WebViewJavascriptBridge.js`
- [ ] **实现全部入站 handler(§8**:44 个,名称/参数结构/同步返回 1:1 对齐;差异项取更完整一侧。
- [ ] **实现全部出站 handler 调用(§9)**:在对应时机以同名 `callHandler` 推送同结构 data。
- [ ] **配置体系(§4**:提供 §4.2 全部配置键取值(建议改用 KV 配置文件,无需沿用目录名编码)。
- [ ] **远程配置请求与分层覆盖(§4.3/4.4)**:URL 拼装、二级 url 二次请求、agent→channel→market→game 后层覆盖、`showmessage` 阻断。
- [ ] **资源管理(§5**:内置包拷贝、version.xml 版本比较、远程 zip 下载/删旧/解压到资源目录。
- [ ] **app_data.js 注入(§6**:加载大厅前在大厅目录生成同名全局变量。
- [ ] **大厅入口(§3**`weburl` 空 → `file://.../gamehall/index.html?Launchtype=0`;非空 → `http://<weburl去-换/>?Launchtype=0`
- [ ] **WebView 能力(§7**JS、DOMStorage、file/universal-file 访问、混合内容、明文 HTTP、禁缓存、禁缩放、定位。
- [ ] **路径 A 子游戏切换(§11.1**`SwitchOverGameData` 在 BridgeWebView 容器内按目录重载、横竖屏切换。
- [ ] **路径 B + 通用网页容器(§11.2/§11.3)**`OpenurlTitleData` 打开独立网页容器;该容器实现 **`settings` 注入对象 6 方法**`backgameData(data)`/`loadurl(urls)`/`browser(url)`/`finishweb()`/`isexitdialogeshow()`/`isbackfinishweb()`+ **3 个 `javascript:` 直调全局函数**`getWebdata(data)`/`gamebackkeydown()`/`backgameData()`)+ 结果码 101 回传上层 → 上层出站 `getWebdata`。**这是与主容器不同的第二套机制,勿遗漏。**
- [ ] **本机 HTTP 上传端点 + `setPostUrl`(§7/§9**:支撑截图分享上传。
- [ ] **设备能力(§10**:分享、微信登录/支付、定位、录音/音效、摇一摇、扫码、相机、震动、剪贴板、网络/电量/WiFi/电话状态。
### 14.2 待决策 / 需补齐(现有 Android 即缺失,新平台应主动完善)
| 事项 | 现状 | 建议 |
|---|---|---|
| QQ / 抖音分享结果回传 | 无回传 | 补齐统一 `sharesuccess` 回传 |
| 抖音 / 快手登录 | 完全未实现(sgapi 是闲聊且注释) | 按需全新对接,回传 code |
| 主动截图能力 | 仅耦合在分享内 | 新增独立 `getCanvasBase64` handler |
| 通讯录 `getAddressBook` | 空实现 | 按需实现或保持空 |
| `notification` 通知 | 空实现 | 按需实现 |
| 微信 AppSecret/API_KEY 明文 | 客户端硬编码 | 下沉服务端,登录改 code 模式 |
| `servertype` / `gameserver` 配置 | 有目录无代码读取 | 可不实现 |
| App 内微信支付 vs 浏览器支付 | 新旧容器不一致 | 统一用 `paybrowser`,按需保留 App 内支付 |
### 14.3 接口名"原样保留"陷阱清单(拼写以代码为准,不可纠正)
- `finsh`(退出,非 finish
- `accreditlogin``friendsSharetypeUrlToptitleDescript``SwitchOverGameData``DragViewvideoIsshow`
- `getcameraaAddress`(双 a)、`getsaomaData``opensaoma`
- `PayuserPaytypePaystate``gameui_play_voice` / `gameui_stop_voice`
- `canclevibrator`(非 cancel)、`repeatvibrator`
- 出站 `backgameData-`(末尾连字符)≠ 入站 `backgameData`
- 下载目录名 `dowlod_zip`(拼写如此)
---
> **结论**:H5 与原生之间的契约 = §2 桥协议 + §8 入站 44 handler + §9 出站约 21 handler + §6 `app_data.js` 注入 + §3~§5 启动与资源流程。新平台(HarmonyOS)只要让这套契约对外表现完全一致,现有 H5 即可**零改动**运行;原生内部实现方式不受任何限制。
+6
View File
@@ -0,0 +1,6 @@
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test
+33
View File
@@ -0,0 +1,33 @@
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
}
}
}
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest",
}
]
}
+6
View File
@@ -0,0 +1,6 @@
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}
+23
View File
@@ -0,0 +1,23 @@
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/source-obfuscation
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
+10
View File
@@ -0,0 +1,10 @@
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {}
}
@@ -0,0 +1,48 @@
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
@@ -0,0 +1,16 @@
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';
const DOMAIN = 0x0000;
export default class EntryBackupAbility extends BackupExtensionAbility {
async onBackup() {
hilog.info(DOMAIN, 'testTag', 'onBackup ok');
await Promise.resolve();
}
async onRestore(bundleVersion: BundleVersion) {
hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion));
await Promise.resolve();
}
}
+23
View File
@@ -0,0 +1,23 @@
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
this.message = 'Welcome';
})
}
.height('100%')
.width('100%')
}
}
+50
View File
@@ -0,0 +1,50 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
],
}
]
}
}
@@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}
@@ -0,0 +1,8 @@
{
"float": [
{
"name": "page_text_font_size",
"value": "50fp"
}
]
}
@@ -0,0 +1,16 @@
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

@@ -0,0 +1,7 @@
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,3 @@
{
"allowToBackupRestore": true
}
@@ -0,0 +1,5 @@
{
"src": [
"pages/Index"
]
}
@@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#000000"
}
]
}
+2
View File
@@ -0,0 +1,2 @@
{
}
@@ -0,0 +1,35 @@
import { hilog } from '@kit.PerformanceAnalysisKit';
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function abilityTest() {
describe('ActsAbilityTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
})
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
})
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
})
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
})
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
})
})
}
@@ -0,0 +1,5 @@
import abilityTest from './Ability.test';
export default function testsuite() {
abilityTest();
}
+11
View File
@@ -0,0 +1,11 @@
{
"module": {
"name": "entry_test",
"type": "feature",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false
}
}
+5
View File
@@ -0,0 +1,5 @@
import localUnitTest from './LocalUnit.test';
export default function testsuite() {
localUnitTest();
}
+33
View File
@@ -0,0 +1,33 @@
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function localUnitTest() {
describe('localUnitTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
});
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
});
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
});
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
});
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
});
});
}
+23
View File
@@ -0,0 +1,23 @@
{
"modelVersion": "6.1.1",
"dependencies": {
},
"execution": {
// "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */
// "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
// "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
// "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
// "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
// "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */
},
"logging": {
// "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
},
"debugging": {
// "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
},
"nodeOptions": {
// "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
// "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
}
}
+6
View File
@@ -0,0 +1,6 @@
import { appTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}
+28
View File
@@ -0,0 +1,28 @@
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0",
"@ohos/hypium@1.0.25": "@ohos/hypium@1.0.25"
},
"packages": {
"@ohos/hamock@1.0.0": {
"name": "@ohos/hamock",
"version": "1.0.0",
"integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==",
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har",
"registryType": "ohpm"
},
"@ohos/hypium@1.0.25": {
"name": "@ohos/hypium",
"version": "1.0.25",
"integrity": "sha512-l6uO2pjl8HyEKdekLqQt7tUpWbDqX/42zoAzkagtUVZAW9jT6lMvbe54MVjoLxq/RwQGygRvi6j4GpypSMFSHw==",
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.25.har",
"registryType": "ohpm"
}
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"modelVersion": "6.1.1",
"description": "Please describe the basic information.",
"dependencies": {
},
"devDependencies": {
"@ohos/hypium": "1.0.25",
"@ohos/hamock": "1.0.0"
}
}