H5 SDK 如何配合 ArkTS Runtime:我做了一个 HarmonyOS JSBridge 框架 Demo

项目地址:https://github.com/lichenyang5/MiniAppRuntime-Harmony
技术栈:HarmonyOS / ArkTS / ArkWeb / JavaScriptProxy / runJavaScript / H5 SDK / HAR
说明:本文介绍的是个人开源学习与工程实践项目,基于公开 HarmonyOS、ArkTS、ArkWeb 能力实现,不包含任何公司内部源码、内部接口、内部文档或非公开实现。


前言

最近我在做一个 HarmonyOS Web 容器与 JSBridge 运行时实践项目。

最开始只是想实现一个简单能力:

H5 页面点击按钮,调用 ArkTS 侧能力,比如弹一个 Toast。

如果只是为了跑通 Demo,其实很简单:

H5 点击按钮
→ ArkTS 收到消息
→ ArkTS 调用系统能力

但如果想把它做成一个更像框架的项目,就不能只写一个按钮和一个 if else

因为真实场景里会遇到这些问题:

H5 怎么描述一次 Native 调用?
多个请求并发时,怎么知道响应属于哪个请求?
H5 需要自己维护 callback 吗?
超时由谁判断?
ArkTS 收到 action 后怎么分发?
参数校验放在哪里?
系统能力调用放在哪里?
ArkTS 怎么统一回调 H5?
错误格式怎么统一?
H5 侧能不能像调用 SDK 一样简单?

所以我把整个项目拆成了两部分:

H5 SDK
→ 解决 H5 怎么舒服地发起调用、等待回调、处理异常

ArkTS Runtime
→ 解决 ArkTS 怎么接收请求、分发 action、调用系统能力、返回统一响应

这篇文章主要记录这个框架的设计思路。


一、整体架构

项目整体可以分成三层:

entry 示例应用
myascf_runtime ArkTS 本地 HAR runtime
h5_sdk H5 侧 JSBridge SDK

它们的职责分别是:

模块 作用
entry HarmonyOS 示例应用,负责启动 ArkWeb、加载 H5、注册 JavaScriptProxy
myascf_runtime ArkTS 侧运行时,负责接收请求、分发 action、调用系统能力
h5_sdk H5 侧 SDK,负责 window.myascf.send、requestId、callback map、timeout、Promise 回调

整体链路如下:

H5 页面
→ H5 SDK:window.myascf.send
→ window.MyASCFNative.postMessage
→ ArkTS JavaScriptProxy
→ BridgeController
→ BridgeDispatcher
→ HandlerRegistry
→ Biz
→ Imp
→ HarmonyOS 系统能力
→ BridgeResponse
→ BridgeCallbackExecutor
→ runJavaScript
→ H5 SDK 收到响应
→ Promise resolve / reject

这条链路看起来比较长,但每一层职责都比较清楚。


二、为什么需要 H5 SDK

一开始 H5 侧可以直接写:

const request = {
  requestId: 'xxx',
  action: 'ui.showToast',
  params: {
    message: 'hello'
  }
}

window.MyASCFNative.postMessage(JSON.stringify(request))

但这样会有几个问题。

第一,每个 H5 页面都要自己生成 requestId

第二,每个 H5 页面都要自己维护 callback。

第三,每个 H5 页面都要自己处理 timeout。

第四,每个 H5 页面都要自己注册 Native 回调函数。

第五,错误处理会散落在业务代码里。

所以我把这些通用逻辑封装进 H5 SDK。

H5 开发者最终只需要写:

await window.myascf.send('ui.showToast', {
  message: 'hello from h5'
})

或者在类型化 SDK 中写:

await api.ui.showToast({
  message: 'hello from h5'
})

这样 H5 业务代码就不需要关心底层通信细节。


三、H5 SDK 具体做了什么

H5 SDK 的核心职责是:

生成 requestId
拼接请求对象
保存 callback
记录开始时间
启动 timeout
调用 window.MyASCFNative.postMessage
接收 ArkTS 回调
根据 requestId 查找 callback
计算 duration
根据 code resolve / reject Promise
处理 TIMEOUT
处理 CALLBACK_LOST
处理 NATIVE_UNAVAILABLE
处理 INVALID_RESPONSE
通知 DebugPanel

它本质上是 H5 侧的 JSBridge 客户端。

一次调用大概是这样:

window.myascf.send('system.storage.getItem', {
  key: 'username'
})

SDK 内部会拼成标准请求:

{
  requestId: 'myascf_1720000000000_1',
  action: 'system.storage.getItem',
  params: {
    key: 'username'
  }
}

然后调用:

window.MyASCFNative.postMessage(JSON.stringify(request))

这里要注意:

window.myascf 是 H5 SDK 暴露的对象。
window.MyASCFNative 是 ArkTS 通过 JavaScriptProxy 注入给 H5 的对象。

所以 H5 SDK 并不是直接调用 ArkTS 方法,而是通过 ArkWeb 的 JavaScriptProxy 通道发送标准请求。


四、requestId 和 callback map

JSBridge 调用是异步的。

比如 H5 连续发出三个请求:

window.myascf.send('ui.showToast', { message: 'A' })
window.myascf.send('system.clipboard.readText', {})
window.myascf.send('system.storage.getItem', { key: 'username' })

这三个请求的返回顺序不一定和发送顺序一致。

所以每次请求都要有一个 requestId

H5 SDK 会维护一个 callback map:

requestId -> callback record

callback record 里通常会保存:

resolve
reject
timer
action
params
createdAt

发送请求前:

生成 requestId
保存 callback
启动 timeout timer
发送请求给 ArkTS

收到响应后:

根据 response.requestId 找 callback
找到后清理 timer
成功则 resolve
失败则 reject
处理完删除 callback

这就是 requestId + callback map 的核心作用。

它解决的是:

异步请求和异步响应如何正确对应的问题。

五、timeout 和 CALLBACK_LOST

H5 SDK 还要处理两个很重要的异常状态:TIMEOUTCALLBACK_LOST

TIMEOUT

H5 发出请求后,会启动一个定时器。

比如:

window.myascf.send('ui.showToast', {
  message: 'hello'
}, {
  timeout: 5000
})

如果 5 秒内 ArkTS 没有回调,H5 SDK 会:

删除 callback
reject TIMEOUT
通知 DebugPanel

所以 TIMEOUT 是 H5 SDK 判断的。

CALLBACK_LOST

如果第 5 秒 H5 已经超时并删除 callback,第 6 秒 ArkTS 才回调回来,这时 H5 SDK 根据 requestId 找不到 callback。

这就是 CALLBACK_LOST

TIMEOUT:H5 等太久,主动放弃等待。
CALLBACK_LOST:响应回来时,H5 已经找不到 callback。

这两个状态都属于 H5 SDK 侧的回调生命周期管理。


六、ArkTS Runtime 负责什么

H5 SDK 只负责把请求发出去,并等待响应。

真正的能力实现放在 ArkTS Runtime。

ArkTS Runtime 做的事情是:

接收 H5 发来的 JSON 字符串
解析 BridgeRequest
根据 action 找 handler
进入 Biz 做参数校验
进入 Imp 调用系统能力
构造统一 BridgeResponse
通过 runJavaScript 回调 H5

也就是说,Runtime 不关心 H5 页面怎么写按钮,也不关心 H5 怎么维护 callback。

Runtime 只关心:

这个 action 是否存在?
参数是否合法?
要调用哪个系统能力?
最终返回什么结果?

七、为什么不把逻辑都写在 BridgeController

最简单的写法是:

if (request.action === 'ui.showToast') {
  // 调 Toast
} else if (request.action === 'system.storage.getItem') {
  // 调 Storage
}

这种写法一开始能跑,但很快会失控。

因为 Controller 会同时承担:

JSON 解析
action 判断
参数校验
系统能力调用
错误处理
回调 H5
日志记录

所以我把 ArkTS Runtime 拆成了几层:

BridgeController
→ 负责入口控制和请求解析

BridgeDispatcher
→ 负责根据 action 分发

HandlerRegistry
→ 负责保存 action 和 handler 的映射

RuntimeBootstrap
→ 负责初始化时注册内置 API

Biz
→ 负责参数校验和业务响应

Imp
→ 负责调用 HarmonyOS 系统能力

BridgeCallbackExecutor
→ 负责统一 runJavaScript 回调 H5

这样每一层只做一类事情。


八、Dispatcher 和 Registry

BridgeDispatcher 的作用是根据 action 找 handler。

伪代码大概是:

const handler = registry.get(request.action)

if (!handler) {
  return UNKNOWN_ACTION
}

return await handler(request)

HandlerRegistry 只负责保存映射关系:

ui.showToast -> ToastBiz.handle
system.clipboard.writeText -> ClipboardBiz.writeText
system.storage.getItem -> StorageBiz.getItem
runtime.getApiList -> RuntimeInfoBiz.getApiList

这些映射是在 Runtime 初始化时由 RuntimeBootstrap 注册进去的。

这样新增 API 时,不需要修改 Controller,只需要:

新增 ActionName
新增 Biz
新增 Imp
在 RuntimeBootstrap 注册
更新 ApiManifest
更新 H5 Demo 和文档

这就是框架扩展性的基础。


九、Biz 和 Imp 为什么分开

每个 API 我都拆成了 Biz 和 Imp。

以 Toast 为例:

ToastBiz
→ 校验 params.message
→ 调用 ToastImp.showToast

ToastImp
→ 调用 promptAction.showToast

以 Storage 为例:

StorageBiz
→ 校验 params.key / params.value
→ 调用 StorageImp

StorageImp
→ 调用 Preferences 本地存储能力

这么做的好处是:

参数校验不和系统能力调用混在一起。
系统能力调用不关心 requestId。
Dispatcher 不需要知道每个 API 的参数规则。
后续换底层实现时,Biz 层可以尽量稳定。

一句话:

Biz 负责“这个调用合不合理”;
Imp 负责“具体怎么调用系统能力”。

十、BridgeResponse:统一返回格式

Runtime 不管成功还是失败,都应该尽量返回统一结构:

{
  requestId,
  code,
  message,
  data
}

成功时:

code = SUCCESS

失败时可能是:

PARSE_ERROR
UNKNOWN_ACTION
PARAM_ERROR
INTERNAL_ERROR

H5 SDK 收到 response 后,只需要看 code

code === SUCCESS -> resolve
code !== SUCCESS -> reject

这样 H5 侧不需要为每个 API 写不同的错误判断逻辑。


十一、CallbackExecutor:统一回调 H5

ArkTS 处理完成后,需要通过 runJavaScript 回调 H5。

如果每个 Biz 或 Controller 都自己拼 JS 字符串,代码会很乱。

所以我单独抽了一个:

BridgeCallbackExecutor

它负责:

序列化 BridgeResponse
拼接 H5 全局回调函数
执行 WebviewController.runJavaScript
记录回调日志
处理回调失败风险

回调方向是:

ArkTS
→ controller.runJavaScript(script)
→ H5 执行 window.__myascf_on_native_response__(response)
→ H5 SDK 根据 requestId 找 callback

这里要注意:

ArkTS 不直接操作 H5 的 callback map。
ArkTS 只是通过 runJavaScript 调用 H5 SDK 注册好的全局回调函数。

十二、ApiManifest 和 runtime.getApiList

当 API 变多后,还会出现一个问题:

代码里注册了哪些 API?
文档里写了哪些 API?
DebugPanel 展示了哪些 API?
H5 SDK 类型里支持哪些 API?

如果这些地方各写一份,很容易不一致。

所以项目里加入了 ApiManifest

它描述每个 API 的元信息:

action
category
title
description
params
response
errors
implemented
biz
imp
example

同时实现了一个运行时自省 API:

await window.myascf.send('runtime.getApiList', {})

它可以把当前 runtime 支持的 API 列表返回给 H5。

这样 DebugPanel 可以动态展示当前支持哪些 API。

runtime.getApiList 不是系统能力 API,它只是 runtime 自己暴露出来的元信息查询能力。


十三、H5 SDK 的 IIFE 和 ESM

H5 SDK 现在支持两种产物。

IIFE 产物

适合普通 H5 页面直接通过 script 引入:

<script src="./js/myascf.js"></script>

加载后自动挂载:

window.myascf

这个适合 HarmonyOS ArkWeb rawfile Demo。

ESM 产物

适合未来前端工程通过 import 使用:

import { initMyASCF, createTypedApi } from 'miniapp-runtime-harmony-web-sdk'

const client = initMyASCF()
const api = createTypedApi(client)

这样就可以使用类型化 API:

await api.ui.showToast({
  message: 'hello'
})

IIFE 解决 Demo 和无构建场景。

ESM 解决未来 npm 包和 TypeScript 工程接入。


十四、H5 SDK 和 Runtime 的分工总结

可以用一张表总结:

模块 负责什么 不负责什么
H5 SDK 生成请求、保存 callback、timeout、resolve/reject、DebugPanel 通知 不调用 HarmonyOS 系统能力
ArkTS Runtime 接收请求、分发 action、校验参数、调用系统能力、返回 BridgeResponse 不管理 H5 callback map
entry Demo 加载 H5、注册 JavaScriptProxy、展示 Demo 页面 不承载框架核心逻辑
ApiManifest 描述 API 元信息 不执行 API
DebugPanel 展示调用记录和 API 列表 不参与核心调用逻辑

这套分工让我更容易理解项目边界。


十五、目前实现的能力

当前已经实现的 API 有:

ui.showToast

system.clipboard.writeText
system.clipboard.readText

system.storage.setItem
system.storage.getItem
system.storage.removeItem
system.storage.clear

runtime.getApiList

同时还有这些工程化能力:

H5 SDK
ArkTS HAR runtime
ApiManifest
API 文档生成
typed API
DebugPanel
npm pack 预检
测试
GitHub Actions CI
Release 文档
面试讲解材料

十六、当前项目边界

这个项目目前仍然是学习和工程实践项目,还没有做:

Network API
Device API
CLI
npm publish
ohpm publish
生产级安全沙箱
完整权限系统

我现在更关注的是把这条链路讲清楚,而不是继续堆功能。

因为一个框架项目的价值不只是 API 数量,而是:

调用链路是否清晰
职责边界是否清楚
扩展 API 是否有规范
错误处理是否统一
文档和代码是否对齐
能不能被别人理解和复用

总结

这个项目最终形成了一个比较清晰的分层:

H5 SDK
→ 负责 H5 侧调用体验

ArkTS Runtime
→ 负责 Native 能力分发和执行

ApiManifest
→ 负责 API 元信息描述

DebugPanel
→ 负责调用链路可视化

entry Demo
→ 负责把整个链路跑起来

H5 SDK 的价值是:

让 H5 开发者不用关心 requestId、callback map、timeout、postMessage 和 Native 回调,只需要调用 window.myascf.send 或 typed API。

ArkTS Runtime 的价值是:

让 Native 能力不是散落在 Controller 里,而是通过 Dispatcher、Registry、Biz、Imp 和 CallbackExecutor 形成稳定调用链路。

用一句话概括:

H5 SDK 解决“怎么调用得舒服”,ArkTS Runtime 解决“怎么把能力稳定地执行并统一返回”。

这就是 MiniAppRuntime-Harmony 当前阶段最核心的框架思路。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐