我在 HarmonyOS 上写了一个轻量级小程序运行时:从 Web 容器到 JSBridge

我在 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)

这个方法做了几件事:

  1. 生成 requestId
  2. 组装请求对象;
  3. 把 Promise 的 resolve / reject 存到 callback map;
  4. 通过 ArkWeb 注入的代理对象调用 ArkTS;
  5. 等 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 容器架构感兴趣,可以一起交流。

相关推荐
Cobyte2 分钟前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
前端H33 分钟前
生成式 UI 实战:AI 如何重塑前端界面
前端·人工智能·ui
懂懂tty40 分钟前
Web前端性能指标
前端
绝世唐门三哥1 小时前
vue3中页面返回时刷新首页的处理方案
开发语言·前端·javascript
东方小月1 小时前
agent-skills:把资深工程师的‘工作流’,装进你的AI编程里
前端·架构·代码规范
Maynor9961 小时前
AI Coding 零基础实战教程|第五部分:完整项目案例实操
java·前端·人工智能·claude code·ai coding
binbin_522 小时前
React 井字棋教程:用一个小游戏理解组件和状态
前端·react.js·前端框架
梦曦i2 小时前
uni-router新推useUniEventChannel,彻底解决页面通信难题
前端·uni-app
柯克七七2 小时前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript
yuanlaile3 小时前
前端转 Flutter 自学完整规划,避开跨栈思维转换误区
前端·flutter·flutter跨平台开发