Files
youle_app_ohos/docs/设计文档/TSGame_HarmonyOS框架设计与开发指南.md
T
lanterngamescnandClaude Opus 4.8 3a333bfe32 docs: 补充横屏项目约束(默认横屏、不支持竖屏自动旋转)
- 契约规范 §7:新增屏幕方向约束——默认横屏、不跟随传感器自动旋转;
  但遵守 H5 零改动铁律,保留 orientation/SwitchOverGameData webtype/通用网页
  orientation 三处 H5 显式方向控制(含竖屏),与原 Android 一致
- 框架指南 §7.4:实现落地——module.json5 orientation:landscape 默认横屏 +
  运行时 window.setPreferredOrientation 按 H5 请求动态切换
- 经核对原 Android:上述三处确为真实竖屏切换(非死代码),故定调为
  '默认横屏 + 不破坏 H5 方向控制'

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

54 KiB
Raw Blame History

TSGame HarmonyOS 应用框架设计与开发指南

配套文档:本设计严格落地《TSGame_原生与H5接口契约总规范》(下称《契约规范》)。契约规范定义"对外必须一致的行为",本文定义"HarmonyOS 侧如何优雅、高效地实现它"。 二者配合即可让现有 H5 零改动运行。

设计目标:现代化(ArkTS / ArkUI 声明式 / Stage 模型)、高性能(Web 保活、并发解压下载、零主线程阻塞)、架构优雅(分层 + 依赖倒置 + 能力插件化)、专业成熟(清晰边界、可测试、可观测、可演进)。

目标平台HarmonyOS NEXTAPI 12+,建议 API 17/23),ArkWeb(方舟 Web)组件。


目录

  1. 设计原则
  2. Android → HarmonyOS 能力映射总表
  3. 总体架构(分层 + 模块)
  4. 工程结构(模块化拆分)
  5. 核心一:JsBridge 桥引擎设计
  6. 核心二:能力插件框架(CapabilityProvider
  7. 容器设计:双容器模型
  8. 启动编排与配置/资源子系统
  9. 性能架构
  10. 横切关注点
  11. 契约可追溯性矩阵
  12. 实施路线图(里程碑)
  13. 验收标准

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 等价物 说明
WebViewX5/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 onPageEndrunJavaScript(桥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.preferencesKV urlpath/upurlpath
本机 HTTP server(截图上传) @ohos.net.http 是客户端、无内置服务端;用 @ohos.net.socket(TCP) 自建轻量 HTTP 服务,或 NAPI 原生 server(如 cpp-httplib 监听本机端口,地址经 setPostUrl 告知 H5
OkHttp @ohos.net.http / rcpRemote Communication Kit 远程配置、下载
zip 解压 @ohos.zlibdecompressFile 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_bridgecontracts 不含任何业务,可被未来其他 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 内 _fetchQueueiframe.src 置为 yy://return/_fetchQueue/<队列JSON>再次经 onLoadIntercepthandleReturnData 路由到该回调。与 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 版本对自定义 schemeyy://)的 iframe 导航是否稳定触发 onLoadIntercept 存在差异。M1 桥引擎自测时,首先用最小回显 H5 确认 yy:// 能被 onLoadIntercept 捕获;若个别版本不触发,回退方案优先级:① onInterceptRequest + WebSchemeHandler(注册自定义 scheme,能力更强、可取 POST 体);② 极端情况下改桥协议为 javaScriptProxy 注入一个 _bridgeNative.postMessage(json) 同步方法替代 yy:// 通道(此法需同步改写注入的 WebViewJavascriptBridge.js_doSend/_fetchQueue,但对 H5 仍透明,window.WebViewJavascriptBridge 对外 API 不变)。三种方案对 H5 均零改动。|

5.2 BridgeController(桥控制器,每个 Bridge 容器持有一个)

职责:管 WebviewController、URL 拦截分发、handler 注册、出入站消息编解码与回调表。对外暴露 registerHandler / callHandler,与 Android 端 API 同名同义

// feature_bridge —— 设计级伪代码(ArkTS 风格,省略 import
export type BridgeHandler = (data: string, callback: (resp: string) => void) => void;

export class BridgeController {
  private controller: webview.WebviewController;
  private registry: HandlerRegistry;                       // 注入:能力层填充
  private responseCallbacks = new Map<string, (d: string)=>void>();
  private startupMessages: Message[] | null = [];          // 页面就绪前的积压
  private uniqueId = 0;

  // —— H5 → 原生:在 Web().onLoadIntercept 调用 ——
  onLoadIntercept(url: string): boolean {
    const u = decodeURIComponent(url);
    if (u.startsWith('yy://return/')) { this.handleReturnData(u); return true; }
    if (u.startsWith('yy://'))        { this.flushMessageQueue(); return true; }
    return false;                                          // 正常导航
  }

  // —— 原生 → H5:能力层/容器调用 ——
  callHandler(name: string, data: string, cb?: (resp: string)=>void): void {
    const m = new Message(); m.handlerName = name; m.data = data;
    if (cb) { const id = `JAVA_CB_${++this.uniqueId}_${SystemClock.now()}`;
              this.responseCallbacks.set(id, cb); m.callbackId = id; }
    this.queue(m);
  }

  registerHandler(name: string, h: BridgeHandler): void { this.registry.put(name, h); }

  // —— 页面就绪:在 Web().onPageEnd 调用 ——
  onPageEnd(): void {
    this.controller.runJavaScript(BRIDGE_JS);              // 注入桥
    if (this.startupMessages) { this.startupMessages.forEach(m => this.dispatch(m));
                                this.startupMessages = null; }
  }

  // —— ⚠️ 关键:触发 H5 交出待发队列。注意队列【不是】靠 runJavaScript 返回值取回,
  //         而是 _fetchQueue() 在 H5 内把 iframe.src 置为 'yy://return/_fetchQueue/<data>'
  //         再次被 onLoadIntercept 捕获 → handleReturnData 路由到这里注册的 '_fetchQueue' 回调。
  private flushMessageQueue(): void {
    // 以函数名 '_fetchQueue' 作为 key 预登记回调(与 callbackId 共用同一张 responseCallbacks 表)
    this.responseCallbacks.set('_fetchQueue', (queueJson: string) => {
      const list = MessageCodec.toArray(queueJson);        // H5 待发消息数组
      for (const m of list) {
        if (m.responseId) {                                // 是 H5 对"原生 callHandler"的回执
          this.responseCallbacks.get(m.responseId)?.(m.responseData);
          this.responseCallbacks.delete(m.responseId);
        } else {                                           // 是 H5 主动 callHandler
          const respFn = m.callbackId
            ? (d: string) => this.queue(Message.response(m.callbackId!, d))
            : (_: string) => {};
          const handler = this.registry.get(m.handlerName) ?? this.registry.default();
          handler(m.data, respFn);                         // ← 分发到能力层
        }
      }
    });
    // 仅触发,不读取返回值
    this.controller.runJavaScript('WebViewJavascriptBridge._fetchQueue();');
  }

  // —— H5 → 原生 的回执通道:yy://return/<functionName>/<data> ——
  // 既处理 _fetchQueue(队列回传),也处理普通 callbackId 回执
  private handleReturnData(url: string): void {
    const fn = parseFunctionFromReturnUrl(url);            // 如 '_fetchQueue' 或 'JAVA_CB_x_y'
    const data = parseDataFromReturnUrl(url);
    const cb = this.responseCallbacks.get(fn);
    if (cb) { cb(data); this.responseCallbacks.delete(fn); }
  }

  private queue(m: Message): void {
    if (this.startupMessages) this.startupMessages.push(m); else this.dispatch(m);
  }
  private dispatch(m: Message): void {
    const json = MessageCodec.escape(m.toJson());
    this.controller.runJavaScript(`WebViewJavascriptBridge._handleMessageFromNative('${json}');`);
  }
}

设计要点:

  • BridgeController 完全不 import 任何能力;能力通过 registry 注入。桥引擎成为独立 HAR,可复用、可单测。
  • 务必忠实复刻"队列经 yy://return/_fetchQueue/ 回传"的机制,不要图省事改读 runJavaScript 的返回值——除非你同时改写注入的 WebViewJavascriptBridge.js(不推荐,破坏与成熟协议的一致性,易在边界场景出错)。responseCallbacks 一张表同时承载"函数名 key(_fetchQueue"与"callbackId keyJAVA_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 线程,若直接 callHandlerrunJavaScript 会抛异常或行为异常。

强制规则

  1. 所有出站下发(dispatch/runJavaScript)必须在 UI 线程执行BridgeController 内部对 dispatch 做线程守卫:非 UI 线程则投递到 UI 线程任务队列。
  2. 入站分发(flushMessageQueue 触发的 handler 回调)默认就在 UI 线程onLoadIntercept 在 UI 线程回调),handler 内若开了子线程做重活,回主线程再 callback/callHandler
  3. 能力 Provider 的异步事件回传统一经 bridge.callHandler,由桥内部保证线程切换,Provider 不需自己关心线程
// BridgeController 内:UI 线程守卫(示意)
private uiContext: UIContext;                 // 由容器在 aboutToAppear/onPageShow 注入
private dispatch(m: Message): void {
  const run = () => {
    const json = MessageCodec.escape(m.toJson());
    this.controller.runJavaScript(`WebViewJavascriptBridge._handleMessageFromNative('${json}');`);
  };
  if (isOnUiThread()) run();
  else this.uiContext.runScopedTask(run);     // 或 emitter/AppStorage 投递回 UI 线程
}

实现方式可选:① UIContext 的 UI 线程任务;② @ohos.events.emitter 在 UI 线程订阅、能力线程 emit;③ AppStorage/LocalStorage 状态驱动。无论哪种,对外保证:能力随便在哪个线程 callHandler,最终都在 UI 线程 runJavaScript

5.6 IWebController 抽象(P2,支撑单测)

桥不直接绑死系统类,依赖最小接口,便于 mock 与未来替换内核:

export interface IWebController {            // 桥只用到这几个能力
  runJavaScript(script: string): void;
  runJavaScript(script: string, cb: (err: Error|null, ret: string)=>void): void;
  loadUrl(url: string): void;
}
// 生产:WebviewControllerAdapter implements IWebController(包一层 webview.WebviewController
// 测试:FakeWebController implements IWebController(记录脚本、模拟 yy:// 回执)

这样 feature_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 插件接口

export interface CapabilityProvider {
  readonly name: string;                                    // 如 'location'
  // 把本能力的入站 handler 注册到桥;并持有 bridge 以便异步推送
  register(bridge: BridgeController, ctx: CapabilityContext): void;
  onForeground?(): void;  onBackground?(): void;            // 生命周期联动
  onDestroy?(): void;
}
export interface CapabilityContext {
  uiAbilityContext: common.UIAbilityContext;
  config: ConfigManager; platform: PlatformServices; log: Logger;
}

6.2 注册表 + 组装根(DI

// entry/di/AppModule.ets —— 组装根:唯一知道"全部能力"的地方
export function buildCapabilities(): CapabilityProvider[] {
  return [
    new ShareProvider(), new LoginProvider(), new PayProvider(),
    new LocationProvider(), new AudioProvider(), new ShakeProvider(),
    new DeviceProvider(), new ClipboardProvider(), new NetworkProvider(),
    new VibrateProvider(), new ScanProvider(), new CameraProvider(),
    new PhotoProvider(), new NavProvider(),                          // OpenurlTitleData/SwitchOverGameData/backgameData
    new AppSystemProvider(),                                          // orientation/browser/finsh/openApplyDownloadpath/notification
    // —— 本期暂不集成:用占位桩注册,保证 H5 调用不报错(见 §6.5),未来换回真实 Provider ——
    new RoomStubProvider(),   // 视频房(声网)暂缓  → 真实版 RoomProvider
    new PayStubProvider(),    // 支付暂缓            → 真实版 PayProvider
    // 闲聊:无 H5 桥接口,无需注册(默认 handler 兜底)
  ];
}
// 容器初始化时:
capabilities.forEach(p => p.register(bridge, ctx));

44 个入站 handler → Provider 归属(确保零孤儿、可逐条核对《契约规范》§8)

Provider 负责的入站 handler
ShareProvider friendsSharetypeUrlToptitleDescript
LoginProvider accreditlogin
PayProvider (本期=PayStubProvider 桩,§6.5 paybrowsergetGameplay(旧版可选)
LocationProvider startlocationgetlocationinfo
AudioProvider prepareaudiomediaTypeAudiosrcIsloopvoicePlaying
ShakeProvider startshakeSwitchShakestopshake
DeviceProvider getTimegetphonestategetbatterygetwifiLevelgetcompareCodegetphoneInfogetmarketnamegetothernamegetOther
ClipboardProvider gameCopytextgamepastetext
NetworkProvider getnetwork
VibrateProvider vibratorrepeatvibratorcanclevibrator
ScanProvider opensaoma
CameraProvider opencamera
PhotoProvider getphoto
RoomProvider (本期=RoomStubProvider 桩,§6.5 createRoomexitRoomgetVideoinfoDragViewvideoIsshow
NavProvider OpenurlTitleDataSwitchOverGameDatagetGameinstallbackgameData(New)
AppSystemProvider orientationbrowserfinshopenApplyDownloadpathnotification(空实现)

合计 44 个入站 handler 全部有且仅有一个归属。出站 handler(§9)由对应 Provider 在事件到达时 bridge.callHandler 推送(如 LocationProvider→getlocationinfoPayProvider→PayuserPaytypePaystateAudioProvider→getaudiourl/gameui_*DeviceProvider→getBattery/getwifiLevel/phonestateScanProvider→getsaomaDataCameraProvider→getcameraaAddressPhotoProvider→getphotoShareProvider→sharesuccessLoginProvider→shareloginShakeProvider→shakeEndNavProvider→getWebdata、容器→appservice/setPostUrl)。

6.3 一个 Provider 的样板(以定位为例)

export class LocationProvider implements CapabilityProvider {
  readonly name = 'location';
  private bridge!: BridgeController; private ctx!: CapabilityContext;

  register(bridge: BridgeController, ctx: CapabilityContext): void {
    this.bridge = bridge; this.ctx = ctx;
    bridge.registerHandler(H.startlocation, (data, _cb) => this.start(data === '1'));
    bridge.registerHandler(H.getlocationinfo, (_d, cb) => cb(this.lastOrError()));
  }
  private start(continuous: boolean): void {
    geoLocationManager.on('locationChange', loc => {
      this.bridge.callHandler(H.getlocationinfo, JSON.stringify(toMapLocationInfo(loc)));
      if (!continuous) geoLocationManager.off('locationChange');
    });
  }
  private lastOrError(): string { /* 返回 MaplocationInfo JSON 或 {errorCode:-1,...} */ }
}

同理:ShareProvider/LoginProvider/PayProvider 对接微信/QQ/抖音/支付 HarmonyOS SDKAudioProviderAVPlayer/SoundPoolScanProvider 用 Scan KitCameraProvider 用 Camera PickerDeviceProvider 用 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 可选用),runJavaScriptcanvas.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 出站。
  • 有同步返回的 handlerH5 传了 responseCallback):必须回一个安全默认值,否则 H5 卡死。如返回 '0'、空串 '' 或合法空 JSON。
  • 绝不触发该能力的出站 handler(如不 callHandler('getVideoinfo')、不 callHandler('PayuserPaytypePaystate')),H5 收不到推送即按"无事发生"处理。

本期占位清单

能力 占位 Provider 入站 handler(注册为桩) 桩行为 关联出站(不触发
视频房(声网) RoomStubProvider createRoomexitRoomgetVideoinfoDragViewvideoIsshow 全部 void,空实现 + 日志 getVideoinfo(uid 推送)
支付 PayStubProvider paybrowsergetGameplay(旧) void,空实现(可轻提示"支付暂未开放" PayuserPaytypePaystate
闲聊 —— 无 H5 接口(《契约规范》§10.4sgapi 是闲聊且全注释、无 registerHandler 无需桩;H5 若误调,走默认 handler 兜底 ——
// 占位 Provider 样板:注册即留空,H5 调用安全无副作用
export class RoomStubProvider implements CapabilityProvider {
  readonly name = 'room(stub)';
  register(bridge: BridgeController, ctx: CapabilityContext): void {
    const noop: BridgeHandler = (_d, _cb) => { ctx.log.i('room not integrated yet'); };
    bridge.registerHandler(H.createRoom, noop);
    bridge.registerHandler(H.exitRoom, noop);
    bridge.registerHandler(H.getVideoinfo, noop);
    bridge.registerHandler(H.DragViewvideoIsshow, noop);
  }
}
export class PayStubProvider implements CapabilityProvider {
  readonly name = 'pay(stub)';
  register(bridge: BridgeController, ctx: CapabilityContext): void {
    bridge.registerHandler(H.paybrowser, (_d, _cb) => ctx.log.i('pay not integrated yet')); // 可 Toast 提示
  }
}

组装根切换buildCapabilities() 里把 new RoomProvider()/new PayProvider() 暂时替换为 new RoomStubProvider()/new PayStubProvider()契约 handler 名/数量不变,未来集成时换回真实 Provider 即可,对 H5 始终零改动、零报错闲聊:本就没有 H5 桥接口,无需任何桩;默认 handler 兜底足以保证任何误调用不报错。


7. 容器设计:双容器模型

《契约规范》明确有两套互不相同的 H5↔原生机制,框架对应两类容器组件

7.1 BridgeGameContainer(大厅 + 子游戏)

对应 webviewActivity/NewwebviewActivity。基于桥协议。

@Component
export struct BridgeGameContainer {
  private controller = new webview.WebviewController();
  private bridge = new BridgeController(this.controller);

  // ⚠️ 仅 debug 包开启远程调试;release 包必须关闭(安全加固,见附录 B)
  aboutToAppear(): void { if (BuildProfile.DEBUG) webview.WebviewController.setWebDebuggingAccess(true); }

  build() {
    Web({ src: this.entryUrl(), controller: this.controller })
      .javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
      .mixedMode(MixedMode.All).cacheMode(CacheMode.None)
      .geolocationAccess(true).zoomAccess(false)
      .onControllerAttached(() => { /* 注入对象(本容器不需要)、设 UA(默认即可) */
                                    buildCapabilities().forEach(p => p.register(this.bridge, ctx)); })
      .onLoadIntercept((e) => this.bridge.onLoadIntercept(e.data.getRequestUrl()))
      .onPageEnd(() => { this.bridge.onPageEnd(); })        // 仅注入桥 + 补发启动队列
      .onProgressChange((e) => { if (e.newProgress === 100 && !this.firstDone) {
          this.firstDone = true;                            // 首次到 100% 才推
          this.bridge.callHandler('appservice', '1');       // 前台
          this.bridge.callHandler('setPostUrl', uploadUrl()); } })
  }
  // 子游戏切换 SwitchOverGameData → 仅 controller.loadUrl(新目录 index.html?Launchtype=1)
}

⚠️ 时序铁律app_data.js 必须在本容器加载页面之前就由 AppDataInjector 写入大厅目录(见 §8.4,发生在 StartupOrchestratorINJECT_APP_DATA 步、早于进入本容器)——因为 H5 首页用 <script src="app_data.js">加载时同步读取切勿放到 onPageEnd/onProgressChange 里推,那时页面已加载完、H5 早已读过 app_data.js,为时已晚。setPostUrl/appservice 则按契约在首次进度 100% 时下发(与 Android onProgressChanged==100 一致)。

  • 入口 URLweburl 空 → file://<解压根>/gamehall/index.html?Launchtype=0;非空 → http://<weburl 去-换/>?Launchtype=0(《契约规范》§3/§5)。
  • 前后台UIAbility.onForeground/onBackgroundcallHandler('appservice','1'|'2')
  • 子游戏切换(路径 ASwitchOverGameData):同容器内 controller.loadUrl() 到目标目录,桥与 handler 不变。

7.2 GenericWebContainer(通用网页容器)

对应 openwebActivity1不挂桥,用 javaScriptProxy 注入 settings 对象 + runJavaScript 直调。

@Component
export struct GenericWebContainer {
  private controller = new webview.WebviewController();
  // settings:H5→原生 6 方法(《契约规范》§11.3)
  private settings = new SettingsProxy(this.controller, /*onReturnData*/ (data)=> this.finishWith101(data));

  build() {
    Web({ src: this.params.url, controller: this.controller })
      .javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
      .geolocationAccess(true).zoomAccess(false)
      .cacheMode(this.online ? CacheMode.Default : CacheMode.Only)    // 有网:按HTTP缓存策略(≈LOAD_DEFAULT);无网:只用缓存(≈LOAD_CACHE_ELSE_NETWORK)。非关键项
      .javaScriptProxy({
        object: this.settings, name: 'settings',
        methodList: ['backgameData','loadurl','browser','finishweb','isexitdialogeshow','isbackfinishweb'],
        controller: this.controller,
      })
      .onProgressChange((e) => { if (e.newProgress === 100 && !this.loaded) {
          this.loaded = true;
          this.controller.runJavaScript(`getWebdata('${this.params.data}')`); } })   // 原生→H5 直调
  }
  // 返回键:若 H5 调过 isbackfinishweb() → runJavaScript('gamebackkeydown()');退出确认 → runJavaScript('backgameData()')
  private finishWith101(data: string) { /* 把 data 经路由/emitter 回传上层 Bridge 容器 → 上层 callHandler('getWebdata', data) */ }
}
  • settings 的 6 方法、getWebdata/gamebackkeydown/backgameData 三个直调函数严格照《契约规范》§11.3。
  • 回传上层:用路由返回值或 emitter 事件把 data 投递回打开它的 Bridge 容器,由后者 callHandler('getWebdata', data)(复刻 Android 结果码 101 语义)。

7.3 file:// 跨域

H5 以 file:// 加载并请求本地/远程资源。HarmonyOS 侧采用官方"Web 页面跨域解决方案":用 onInterceptRequest 自定义本地资源响应,或为本地页配置自定义协议/响应头,避免 file 同源限制。对 H5 透明

7.4 屏幕方向(横屏项目)

本项目为横屏项目:应用默认横屏、不跟随设备传感器自动旋转;同时遵守 H5 零改动铁律,完整保留 H5 经方向接口显式切换(含竖屏)的能力,与原 Android 一致(《契约规范》§7 屏幕方向约束 / §8.4 / §11)。落地方式:

  • 默认横屏EntryAbilitymodule.json5"orientation": "landscape"(启动即横屏、不随重力感应自动转竖屏)。
  • H5 动态切换(保留竖屏能力):方向接口收到请求时用窗口 API 动态覆盖,不写死锁定——
    • AppSystemProviderorientation handlerdata === '1' → 横屏,否则 → 竖屏(对齐 Android setRequestedOrientation);
    • 子游戏 SwitchOverGameData:按 webtype"3" 横 / "2" 竖)设方向后再 loadUrl
    • GenericWebContainer:按打开参数 orientation"1" 竖 / 其他 横)设方向。
  • 实现 API:运行时 window.Window.setPreferredOrientation(orientation: window.Orientation) 动态切换,window.OrientationLANDSCAPE / PORTRAIT(编码前用 devecocli docs 核对签名)。
  • 要点module.json5landscape 提供"默认横屏 + 禁传感器自动转",运行时 setPreferredOrientation 提供"H5 显式切换"——二者叠加既满足横屏项目定位,又守住 H5 零改动。

8. 启动编排与配置/资源子系统

8.1 StartupOrchestrator(启动状态机)

把《契约规范》§3 时序实现为显式状态机,每步可重试、可观测、可降级:

INIT → LOAD_LOCAL_CONFIG → REQUEST_PERMISSIONS → FETCH_REMOTE_CONFIG
     → RESOLVE_VERSION → [PREPARE_RESOURCE: 内置拷贝/zip下载解压] → INJECT_APP_DATA
     → ENTER_HALL (跳 BridgeGameContainer)
   任一步失败 → FALLBACK(本地缓存) 或 BLOCK(showmessage 公告)
  • showmessage 非空 → 阻断弹窗(§3.2)。
  • 远程配置长度 <30 视为错误文案直接提示。

8.2 ConfigManager(配置)

  • 本地配置:弃用"目录名编码",改为随包内置 resources/rawfile/app_config.json(KV),提供《契约规范》§4.2 全部键:agent/channel/gamedir/gamestart/appversion/market/gameid/weburl/gameconfig/other/tuiguang。对 H5 无感知。
  • 远程配置HttpClient.get("http://"+gameconfig.replace(/-/g,'/')+".txt?a="+ts),禁缓存。
  • 分层覆盖VersionResolver 实现 agent→channel→market→game 后层覆盖(§4.4),含 agentlist/gamelist 两棵树与二级 url 二次请求。用纯函数实现,便于单测。

8.3 ResourceManager(资源)

路径(@ohos.file.fs + context.filesDir):
  解压根 urlpath = <filesDir>/tsgames/<bundle>/<时间戳>/<gamedir>
  游戏目录       = <urlpath>/gamehall
  入口           = file://<urlpath>/gamehall/index.html
  KV 持久化       urlpath / upurlpath@ohos.data.preferences
流程:
  首启 → 拷贝内置包(rawfile) → @ohos.zlib.decompressFile 解压
  非首启 → 比较 version.xml<version value> 与远程 game_version
  需更新 → Downloader 下载 game_download(?a=ts) → 删旧 → decompressFile → 收尾
  • 下载、解压在 TaskPool/Worker 执行(§9),UI 线程只收进度回调。
  • version.xml 解析用 @ohos.xmlXmlPullParser),取 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、可序列化对象),不能传:闭包回调、WebviewControllerUIAbilityContext、复杂业务对象。因此下载/解压的"进度回传"不能直接传 callback 进 TaskPool。落地范式:

// 1) 重活在 @Concurrent 函数里跑,只接收/返回 Sendable 数据
@Concurrent
function unzipTask(zipPath: string, destDir: string): boolean { /* @ohos.zlib.decompressFile */ return true; }

// 2) 进度/完成经 emitter 回 UI 线程(不跨线程传函数)
@Concurrent
function downloadTask(url: string, savePath: string, eventId: number): void {
  // 边下边 emitter.emit({eventId}, { data: percent })  ← 仅传 Sendable 数字
}
// UI 线程:emitter.on({eventId}, e => updateProgressUI(e.data.percent));
//          taskpool.execute(downloadTask, url, savePath, eventId);

要点:TaskPool 只搬 Sendable 数据,UI 反馈一律走 emitter/AppStorage。这条不遵守,编译期/运行期必报错。context.filesDir 等路径应在 UI 线程取好后以字符串传入。


10. 横切关注点

关注点 设计
契约类型(SSOT contracts HARenum 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 ① 最小回显 H5callHandler('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_bridge100% 复刻 lzyzsd 协议)为基石,能力以 CapabilityProvider 插件化挂载,配置/资源/启动以领域服务编排,双容器分别承载"桥协议"与"settings 注入"两套契约——这套现代化 ArkTS 分层框架能在不改任何 H5 的前提下,优雅、高效地承载 TSGame 全部业务,并为后续能力补齐与多渠道演进留足空间。


附录 AArkTS / ArkUI 实现适配清单

前文为"设计级"。本附录把设计落到 ArkTS/ArkUI 的现实工程约束上,照此即可从"设计就绪"过渡到"编码就绪"。

A.1 ArkUI @Component struct 与桥/控制器的协作

  • 持有方式WebviewControllerBridgeController 作为 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/onPageHideprovider.onForeground/onBackgroundcallHandler('appservice','1'|'2')

A.2 ArkTS 严格语法注意

  • 全量类型标注,禁用 anyDTO 用 interfacehandler 名用 enum/constcontracts 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.ArkWebwebview.WebviewControllerWeb
偏好存储(urlpath 等) @ohos.data.preferences
文件 IO @ohos.file.fs
解压 @ohos.zlibdecompressFile
XMLversion.xml @ohos.xmlXmlPullParser
网络请求/下载 @ohos.net.http@kit.RemoteCommunicationKitrcp
本机端口服务(截图上传) @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.MediaKitAVPlayer/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.json5abilities.skills.uris 声明
权限合规 定位/相机/麦克风/电话用时申请 + 文案说明;高德等 SDK 隐私合规初始化(updatePrivacyShow/Agree)先于使用
渲染兜底 onRenderExited 重载页面,防子进程崩溃白屏
数据安全 KV/文件按需加密;日志脱敏(不打 openid/token 明文)
上架审核 准备隐私声明、权限用途清单,符合 HarmonyOS 应用市场审核要求