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