H5 SDK 如何配合 ArkTS Runtime:我做了一个 HarmonyOS JSBridge 框架 Demo
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 还要处理两个很重要的异常状态:TIMEOUT 和 CALLBACK_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 当前阶段最核心的框架思路。
更多推荐


所有评论(0)