我在 HarmonyOS 上写了一个轻量级小程序运行时:从 Web 容器到 JSBridge
项目地址:github.com/lichenyang5...
项目定位:一个受小程序运行时架构启发的 HarmonyOS Web 容器与 JSBridge 框架。
说明:本文只讨论个人开源项目的设计思路,全部基于公开 HarmonyOS / ArkTS / ArkWeb 能力实现,不包含任何公司内部源码、内部接口或非公开文档内容。
1. 为什么要做这个项目
很多前端同学第一次接触 HarmonyOS,会有一个很自然的问题:
能不能像小程序一样,在原生应用里面承载一套 H5 页面,然后让 H5 调用原生能力?
这件事听起来简单,其实背后有一条很完整的运行时链路。
H5 页面只是入口,真正的问题是:
- H5 怎么把请求发给 ArkTS?
- ArkTS 怎么知道这个请求要调用哪个能力?
- 不同能力怎么注册、分发和维护?
- H5 调完之后,结果怎么回到 Promise?
- 超时、未知 action、参数错误、回调丢失这些问题怎么处理?
所以,我做了这个项目:MiniAppRuntime-Harmony。
它不是为了做一个大而全的小程序平台,而是先做一条最小、清晰、可解释、可扩展的链路。
第一阶段的目标非常明确:
text
ArkWeb 加载本地 H5
→ H5 调用 window.myascf.send(action, params)
→ ArkTS 接收请求
→ Dispatcher 分发 action
→ Registry 找到 handler
→ Biz 做参数校验
→ Imp 调用系统能力
→ ArkTS 通过 runJavaScript 回调 H5
→ H5 Promise resolve / reject
2. 最小闭环长什么样
现在 Demo 里已经跑通了一个能力:
js
window.myascf.send("ui.showToast", {
message: "hello from h5"
})
H5 调用这行代码后,最终会在 HarmonyOS 侧触发真实的 promptAction.showToast。
这背后的链路是:
text
H5 Button
→ myascf.js
→ window.MyASCFNative.postMessage(...)
→ JavaScriptProxy.postMessage(...)
→ BridgeController
→ BridgeDispatcher
→ HandlerRegistry
→ ToastBiz
→ ToastImp
→ promptAction.showToast
→ BridgeCallbackExecutor
→ runJavaScript
→ H5 callback map
→ Promise resolve
可以看到,这不是简单写一个 if (action === "xxx")。
我希望这个项目从第一天开始,就具备框架感:
- Web 容器只负责加载页面;
- BridgeController 只负责通信边界;
- Dispatcher 只负责分发;
- Registry 只负责注册和查询;
- Biz 只负责业务语义和参数校验;
- Imp 只负责调用 HarmonyOS 系统能力;
- CallbackExecutor 只负责把结果安全回调给 H5。
这样写的好处是,后面新增 API 时,不会把所有逻辑堆在一个页面里面。
3. 项目目录
目前核心目录大致如下:
text
entry/src/main/ets/runtime/
bridge/
JavaScriptProxy.ets
BridgeController.ets
BridgeCallbackExecutor.ets
dispatcher/
BridgeDispatcher.ets
registry/
HandlerRegistry.ets
RuntimeBootstrap.ets
api/
ActionNames.ets
BridgeTypes.ets
biz/
ToastBiz.ets
imp/
ToastImp.ets
error/
BridgeErrorCode.ets
BridgeError.ets
logger/
RuntimeLogger.ets
H5 Demo 放在:
text
entry/src/main/resources/rawfile/web/
index.html
js/
myascf.js
demo.js
css/
demo.css
文档放在:
text
docs/
overview/
architecture/
stages/
api/
debug/
这也是我比较推荐的做法:代码和文档同步推进。
很多个人项目的问题是,代码写得越来越多,但别人看不懂。
这个项目每做一步,都会对应一篇 stage 文档,解释"为什么这么做"。
4. 第一步:先让 ArkWeb 加载本地 H5
这个项目没有一上来就写 JSBridge。
第一步只做了一件事:让 HarmonyOS 应用通过 ArkWeb 加载本地 HTML。
ts
Web({
src: $rawfile('web/index.html'),
controller: this.controller
})
这一点很重要。
因为如果 Web 容器都没跑稳,后面调 JSBridge、Dispatcher、Registry,只会把问题混在一起。
所以第一阶段的节奏是:
text
先跑通页面
再跑通 H5 → ArkTS
再引入 action 分发
再接真实系统能力
最后处理 timeout 和 callback lost
每一步都可以单独验证。
5. 第二步:H5 如何调用 ArkTS
H5 侧暴露了一个统一入口:
js
window.myascf.send(action, params)
这个方法做了几件事:
- 生成
requestId; - 组装请求对象;
- 把 Promise 的
resolve / reject存到 callback map; - 通过 ArkWeb 注入的代理对象调用 ArkTS;
- 等 ArkTS 回调后,根据
requestId找到对应 callback。
请求协议很简单:
json
{
"requestId": "req_xxx",
"action": "ui.showToast",
"params": {
"message": "hello from h5"
}
}
响应协议也很简单:
json
{
"requestId": "req_xxx",
"code": 0,
"message": "showToast success",
"data": {
"echoAction": "ui.showToast"
}
}
这里的关键点是 requestId。
没有 requestId,H5 同时发出多个请求时,就不知道哪个响应对应哪个 Promise。
6. 第三步:不要在 Controller 里写业务
一开始,为了验证通信,BridgeController 可以直接返回 mock response。
但是很快会遇到问题:
如果所有 action 都写在 BridgeController 里,它就会变成一个巨大的上帝类。
所以项目引入了两个东西:
text
BridgeDispatcher
HandlerRegistry
Dispatcher 做分发,Registry 做注册。
伪代码是这样的:
ts
const handler = registry.get(request.action)
if (!handler) {
return unknownActionResponse
}
return await handler(request)
这样一来,BridgeController 的职责就变轻了。
它只负责:
- 接收原始消息;
- 解析请求;
- 调用 Dispatcher;
- 把响应回给 H5。
它不关心 ui.showToast 到底怎么实现。
7. 第四步:Biz / Imp 分层
接下来,ui.showToast 从 mock handler 变成真实能力。
但是我没有在 handler 里面直接写:
ts
promptAction.showToast(...)
而是拆成两层:
text
ToastBiz
ToastImp
ToastBiz 负责参数校验:
text
message 是否存在?
message 是否是字符串?
message 是否为空?
ToastImp 负责调用系统能力:
ts
promptAction.showToast({
message
})
这样拆的好处是:
- 参数校验和系统调用分离;
- 以后测试 Biz 时,可以 mock Imp;
- 以后 Toast 能力变复杂时,不会污染 Dispatcher;
- 以后新增 API,可以复制这套模式。
这也是框架项目和普通 Demo 的区别。
8. 第五步:JSBridge 不是发消息就完了
能发消息,只是第一步。
真正写 JSBridge 时,还要处理:
- H5 调用超时;
- ArkTS 回调时 callback 已经不存在;
- ArkTS 回调 JS 字符串转义;
- 未知 action;
- 参数错误;
- 内部异常。
所以项目又抽出了一层:
text
BridgeCallbackExecutor
它专门负责:
- 把 BridgeResponse 序列化;
- 安全拼接 JS;
- 调用
runJavaScript; - 记录回调日志;
- 处理回调失败风险。
H5 侧则补充了 timeout:
js
window.myascf.send("ui.showToast", params, {
timeout: 5000
})
如果超过时间还没收到响应,Promise 会 reject 一个 TIMEOUT。
如果响应回来时 callback 已经被删除,就记录 CALLBACK_LOST,而不是让页面崩掉。
9. 目前项目完成度
目前已经完成:
- ArkWeb 本地 H5 加载;
- H5 Promise 形式调用;
- JavaScriptProxy 通信边界;
- BridgeController;
- BridgeDispatcher;
- HandlerRegistry;
- RuntimeBootstrap 注册;
- ToastBiz 参数校验;
- ToastImp 调用
promptAction.showToast; - BridgeCallbackExecutor;
- TIMEOUT;
- CALLBACK_LOST;
- UNKNOWN_ACTION;
- PARAM_ERROR;
- INTERNAL_ERROR;
- 分阶段 docs 文档。
这已经是一个比较完整的第一阶段运行时骨架。
它还不是一个完整小程序框架,但已经能说明一件事:
一个 HarmonyOS Web 容器框架,应该如何从通信边界、能力注册、业务分层和回调机制一步步搭起来。
10. 后续计划
下一步我准备继续做两件事。
第一,扩展第二个 API,例如:
text
system.clipboard.writeText
system.clipboard.readText
这样可以验证 Registry / Biz / Imp 模式是否可复用。
第二,做一个轻量调试面板,展示:
- requestId;
- action;
- params;
- response;
- duration;
- code;
- error message。
这会让整个运行时更适合调试,也更像一个真实框架。
项目地址:
text
https://github.com/lichenyang5/MiniAppRuntime-Harmony
如果你也在学习 HarmonyOS / ArkTS / ArkWeb,或者对 JSBridge、小程序运行时、Web 容器架构感兴趣,可以一起交流。