说明:这篇文章不是公司闭源 ASCF 源码解析,而是基于 HarmonyOS ArkWeb 官方文档、ASCF/原子服务公开说明,以及我自己的
harmony-ASCF-demo梳理出来的学习笔记。目标是把"为什么需要双线程通信、JavaScriptProxy 和 runJavaScript 分别干什么"讲清楚。

1. 我一开始的误解
刚接触 ASCF 的时候,我以为 H5 调鸿蒙能力就是一句:
js
window.ascfBridge.send(...)
然后 ArkTS 收到以后返回结果,页面展示。
后来写 demo 才发现,这里面其实不是一条简单的函数调用链,而是两条方向相反的通信链路:
text
H5 → ArkTS:javaScriptProxy
ArkTS → H5:runJavaScript
这两条链路拼起来,才是一个完整的 JSBridge 闭环。
如果只看到 window.ascfBridge.send(),很容易误以为"前端直接调到了鸿蒙能力"。实际上,H5 调到的只是 ArkTS 暴露出来的一个桥接方法,真正的能力调用、参数校验、权限判断、结果封装,都发生在宿主侧。
2. 为什么需要"双线程"这个概念?
可以先从小程序的运行模型理解。
普通 H5 页面里,页面渲染、DOM 操作、业务 JS、用户点击事件,很多东西都混在 WebView 里。这样做很方便,但问题也明显:
- 业务 JS 太重,页面可能卡。
- H5 可以直接操作页面环境,平台不容易管控。
- Native 能力如果直接暴露给页面,安全风险会很大。
- 页面展示和底层能力调用混在一起,后期很难维护。
所以类小程序框架通常会把运行环境拆开:
text
渲染层:负责页面显示、用户交互、WebView 渲染
逻辑层:负责业务逻辑、API 调用、生命周期、数据处理
放到我现在理解的 ASCF 场景里,可以先这样记:
text
UI / 渲染层:WebView 里的 H5 页面
逻辑 / 宿主层:ArkTS 容器、JSBridge、Dispatcher、Native 能力
这里的"双线程"不是让我们死记线程名字,而是理解一种设计思想:页面负责展示,宿主负责能力,二者通过桥接协议通信。
3. ASCF 里一次调用到底怎么走?
以我的 demo 为例,H5 页面里有一个按钮:
js
function callDeviceInfo() {
run('getDeviceInfo')
}
点击后会进入统一的调用方法:
js
ascf.call(action, params, options)
然后它会生成一个请求对象:
js
{
version: '1.0',
id: 'req_001',
action: 'getDeviceInfo',
params: {},
timeout: 5000
}
最后发给 ArkTS:
js
window.ascfBridge.send(JSON.stringify(req))
这一句就是 H5 调 ArkTS 的入口。
但问题来了:window.ascfBridge 是哪里来的?
它不是浏览器天然存在的对象,而是 ArkTS 侧通过 Web 组件的 javaScriptProxy 注入进去的。
大概像这样:
ts
.javaScriptProxy({
object: this.bridge,
name: 'ascfBridge',
methodList: ['send'],
controller: this.controller
})
这段配置的意思可以理解为:
text
把 ArkTS 里的 this.bridge.send 方法,
暴露给 H5 页面,
在 H5 里叫 window.ascfBridge.send。
注意,不是把整个 WebviewController 暴露给 H5,也不是把 ArkTS 所有方法都扔给页面。H5 能调什么,取决于 methodList 里暴露了什么。
所以如果只写了:
ts
methodList: ['send']
那么 H5 侧能调用的就是:
js
window.ascfBridge.send(...)
而不是:
js
window.ascfBridge.dispatch(...)
window.ascfBridge.register(...)
window.ascfBridge.runJavaScript(...)
这点很重要。桥接层暴露得越少,安全边界越清楚。
4. JavaScriptProxy 负责 H5 → ArkTS
javaScriptProxy 做的事情,可以用一句话概括:
text
把 ArkTS 对象的方法注册到前端页面,让 H5 可以调用应用侧方法。
所以这条链路是:
text
H5 页面
↓ window.ascfBridge.send(request)
javaScriptProxy
↓
ArkTS bridge.send(message)
↓
BridgeController
↓
Dispatcher
↓
Biz / Imp
↓
Native 能力或模拟能力
在我的 demo 里,send 收到字符串以后,不会直接执行业务,而是进入统一流程:
text
1. 解析 JSON
2. 校验 version / id / action / params
3. 根据 action 分发
4. 到 Registry 里找对应 handler
5. 进入 Biz 层处理业务语义
6. 进入 Imp 层执行具体能力
7. 生成统一 response
这样做的好处是,H5 只需要知道:
js
ascf.call('getDeviceInfo')
它不需要关心:
- 鸿蒙设备信息 API 怎么调用
- 是否需要权限
- 返回格式怎么封装
- 出错时错误码怎么定义
- 异步回调怎么对应到原来的请求
这些复杂度都应该由 ASCF 框架层处理。
5. runJavaScript 负责 ArkTS → H5
H5 发出去之后,ArkTS 处理完能力,还要把结果还给 H5。
这时候就轮到 runJavaScript 了。
H5 里会提前挂一个全局回调函数:
js
window.__ascfOnResponse = function (jsonStr) {
ascf._onResponse(jsonStr)
}
这行代码不是 H5 自己主动调用的,而是给 ArkTS 留的"回调入口"。
ArkTS 侧处理完之后,会类似这样调用:
ts
this.controller.runJavaScript(
`window.__ascfOnResponse(${JSON.stringify(responseJson)})`
)
于是 H5 侧的 window.__ascfOnResponse 被执行,拿到 ArkTS 回来的响应。
所以第二条链路是:
text
ArkTS response
↓
WebviewController.runJavaScript(...)
↓
window.__ascfOnResponse(jsonStr)
↓
ascf._onResponse(jsonStr)
↓
pending[id]
↓
resolve / reject
↓
then / catch
↓
showResult(resp)
↓
页面展示
这就是为什么我说 JSBridge 不是一条链路,而是两条链路拼起来:
text
H5 调 ArkTS:javaScriptProxy
ArkTS 回 H5:runJavaScript
6. requestId 为什么重要?
刚开始写 demo 的时候,我容易忽略 requestId。
后来发现,没有它就没法处理异步。
比如 H5 连续点了三个按钮:
text
getDeviceInfo
getLocation
getClipboardData
这三个请求可能不是按发送顺序返回的。如果没有 id,H5 就不知道哪个 response 对应哪个按钮。
所以 H5 发请求时要生成 id:
js
var req = {
id: 'req_001',
action: 'getDeviceInfo',
params: {}
}
同时在本地保存一个 pending 表:
js
pending[id] = {
resolve,
reject,
timer,
action
}
等 ArkTS 回来时:
js
var p = pending[resp.id]
如果找到了,就说明这次响应能对应到之前的请求,然后再执行:
js
resp.code === 0 ? p.resolve(resp) : p.reject(resp)
最后页面里的 .then() 或 .catch() 才会继续执行。
所以页面展示不是 __ascfOnResponse 直接改 DOM,而是:
text
__ascfOnResponse
↓
找到 pending
↓
resolve / reject
↓
then / catch
↓
showResult
这也是我之前看代码时卡住的地方:我看到了 window.__ascfOnResponse,但没看到它在哪里展示数据。真正展示数据的是后面的 showResult(resp)。
7. Dispatcher / Register / Biz / Imp 是干什么的?
如果只是 demo,其实可以在 send 里直接写:
ts
if (action === 'getDeviceInfo') {
return getDeviceInfo()
}
但这样越写越乱。
真实框架一般会拆成:
text
Register:启动时注册能力
Dispatcher:运行时根据 action 找能力
Biz:处理业务语义
Imp:执行具体实现
例如:
text
getDeviceInfo → DeviceHandler
getCurrentTime → TimeHandler
getClipboardData → ClipboardHandler
setClipboardData → ClipboardHandler
openToast → ToastHandler
这样 H5 发来:
json
{
"id": "req_001",
"action": "getDeviceInfo",
"params": {}
}
Dispatcher 就去 Map 里找:
text
Map.get('getDeviceInfo')
找到了就执行,找不到就返回:
text
UNKNOWN_ACTION
这也是维护 ASCF 框架时非常常见的问题:H5 说"我调了,但是没反应",你第一步就可以查 action 有没有注册、拼写是否一致、参数是否符合协议。
8. 我现在怎么理解 ASCF 双线程?
现在我会这样理解:
text
ASCF 不是简单地把 H5 放进 WebView。
它更像是在 WebView 和 HarmonyOS 能力之间,加了一层受控的运行时。
H5 不直接碰 Native 能力,而是:
text
H5 → JSBridge → ArkTS 宿主 → Dispatcher → Biz/Imp → Native 能力
Native 也不是随便把结果塞回页面,而是:
text
Native 结果 → 统一 response → runJavaScript → H5 callback → Promise → 页面更新
所以 ASCF 双线程通信的核心,不是"线程"这两个字,而是这三个点:
text
1. 渲染和逻辑分离
2. 能力调用走协议
3. 双向通信有边界
9. 这套模型对排查问题有什么帮助?
理解这条链路以后,排查问题就不会乱猜。
如果 H5 调不到 ArkTS,先查:
text
javaScriptProxy 是否注册成功?
methodList 是否包含 send?
H5 是否在 Web 容器里打开?
window.ascfBridge 是否存在?
如果 ArkTS 收到了但没有结果,查:
text
action 是否正确?
Registry 里是否注册?
Dispatcher 是否找到 handler?
Biz / Imp 有没有抛错?
如果 ArkTS 执行成功但 H5 没显示,查:
text
runJavaScript 是否执行?
window.__ascfOnResponse 是否存在?
response.id 是否和 pending 里的 id 一致?
是否已经超时删除 pending?
showResult 是否执行?
这比单纯看日志有效很多,因为你知道每一段链路的职责。
10. 总结
这篇文章可以用一句话收尾:
text
JavaScriptProxy 解决 H5 如何调用 ArkTS;
runJavaScript 解决 ArkTS 如何回调 H5;
Dispatcher / Register / Biz / Imp 解决 ArkTS 内部如何把 action 分发到具体能力。
所以完整闭环是:
text
H5 按钮点击
↓
window.ascfBridge.send
↓
javaScriptProxy
↓
ArkTS bridge.send
↓
Controller / Protocol
↓
Dispatcher / Register
↓
Biz / Imp
↓
response
↓
runJavaScript
↓
window.__ascfOnResponse
↓
Promise resolve / reject
↓
页面展示
如果后面继续维护 ASCF 框架,我觉得重点不是背 API,而是把这条链路跑熟。
因为真实项目里的问题,大概率就出在这几类地方:
text
桥没有注入
action 没注册
协议不一致
权限没过
异步回调丢失
response id 对不上
页面销毁后还在回调
把这些问题串起来,ASCF 就不再是一堆陌生名词,而是一条可以一步步排查的通信链路。
官方参考
-
HarmonyOS ArkWeb:前端页面调用应用侧函数
-
HarmonyOS ArkWeb:应用侧调用前端页面函数
-
HarmonyOS ArkWeb:WebviewController API 参考
-
HarmonyOS ArkWeb:组件安全开发建议
-
ASCF Development Guide