H5 调 ArkTS、ArkTS 再回调 H5:ArkWeb 双向通信完整实践【鸿蒙心迹】

大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
混合应用里有一类很典型的需求:页面主体由 H5 实现,但登录态、设备能力、系统页面跳转等能力掌握在应用侧。H5 需要调用 ArkTS,ArkTS 处理完成后又要把结果送回网页。
如果只是单向通知,这件事并不复杂。真正容易把代码写乱的是“H5 发起请求 → ArkTS 接收参数 → 执行业务 → ArkTS 回调 H5 → H5 恢复对应 Promise”这一整条链路。
这次用一个本地 H5 最小示例,把 ArkWeb 中这条双向通信链路完整串起来,并进一步封装成一个统一 Bridge。
一、为什么混合应用需要 JS Bridge
先看一个具体场景。
假设应用中有一个活动页由 H5 开发。页面需要两个能力:
- H5 传入两个数字,让 ArkTS 完成计算并立即返回;
- H5 发起一个异步请求,ArkTS 处理完成后主动回调 H5。
这两个需求对应两条方向相反的通道:
H5 -> ArkTS
JavaScriptProxy / registerJavaScriptProxy
ArkTS -> H5
WebviewController.runJavaScript()
华为 ArkWeb 官方开发指南把“应用侧调用前端页面函数”“前端页面调用应用侧函数”和“建立应用侧与前端页面数据通道”分别作为 Web 与 JavaScript 交互能力进行说明。对于持续的消息型通信,官方还提供 createWebMessagePorts() 创建消息端口。
本文不做消息端口方案,而是聚焦更接近传统 JS Bridge 的调用模型:
H5
│
│ window.NativeBridge.invoke(...)
▼
ArkTS
│
│ runJavaScript(...)
▼
H5 callback
这样做的好处是业务层可以继续使用“方法名 + 参数 + Promise”的调用习惯,而不用让每个 H5 页面自己拼 ArkWeb API。
二、先确认版本和能力边界
本文以 HarmonyOS 7、API 26、Stage 模型应用作为目标开发背景。
华为官方已经明确,HarmonyOS 7.0 对应 API 26.0.0;从 26.0.0 开始,HarmonyOS 开发套件的 API 版本号改用 X.Y.Z 语义化版本格式。官方同时建议面向 HarmonyOS 7 的应用使用 26.0.0 开发套件进行升级适配。
这里使用的核心模块是:
import { webview } from '@kit.ArkWeb';
核心对象是:
webview.WebviewController
本文涉及的主要能力包括:
| 能力 | 用途 |
|---|---|
Web | 在 ArkUI 页面中承载 H5 |
$rawfile() | 引用应用包内的本地网页资源 |
javaScriptProxy() | Web 初始化时把 ArkTS 对象注册到网页环境 |
runJavaScript() | 应用侧执行当前网页上下文中的 JavaScript |
deleteJavaScriptRegister() | 删除已经注册的 JavaScriptProxy 对象 |
runJavaScript() 是异步执行接口,执行结果通过 Promise 返回;官方 FAQ 中也明确说明,它在当前显示页面上下文执行 JavaScript,并要求在 UI 线程使用。
还有一个边界必须单独说明:**本文讨论的是 HarmonyOS 应用中的 ArkWeb Web 组件,不是元服务的 AtomicServiceEnhancedWeb。**截至 2026 年 9 月的官方 FAQ,AtomicServiceEnhancedWeb 暂不支持 runJavaScript() 和 registerJavaScriptProxy(),不能把本文代码直接套到该组件上。
这个区别很容易被忽略。
三、先搭一个最小实践
工程中准备一个本地网页:
entry
└── src
└── main
├── ets
│ └── pages
│ └── Index.ets
└── resources
└── rawfile
└── bridge
└── index.html
本文只加载应用包中的 $rawfile() 本地页面,把网络请求、登录、路由等业务全部拿掉。
最终希望实现两个调用:
HarmonyBridge.call('sum', {
a: 10,
b: 20
});
立即得到:
30
再调用:
HarmonyBridge.call('delayEcho', {
message: 'Hello ArkTS'
});
ArkTS 先接收请求,异步处理结束后再主动执行 H5 中的回调函数,让这个调用最终仍然表现为一个 Promise。
四、先实现 H5 调 ArkTS
1. 定义统一请求结构
如果每增加一个能力就往 window 上暴露一个方法:
getUser()
openPage()
scan()
pay()
getLocation()
...
Bridge 很快就会失去边界。
更容易维护的办法是只暴露一个入口:
NativeBridge.invoke(requestJson)
请求统一成:
{
"id": "req_001",
"method": "sum",
"params": "{\"a\":10,\"b\":20}"
}
id 用于异步结果匹配,method 表示要调用的能力,params 保存业务参数。
2. ArkTS 侧 Bridge
下面代码按照 ArkWeb 官方 JavaScriptProxy 和 runJavaScript() 的接口形式组织为最小示例。由于这里没有实际执行 HarmonyOS 工程编译,发布前仍应使用目标 API 26 SDK 做一次工程级校验。
import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';
interface BridgeRequest {
id: string;
method: string;
params: string;
}
interface BridgeResponse {
id: string;
ok: boolean;
pending: boolean;
data: string;
error: string;
}
interface SumParams {
a: number;
b: number;
}
interface EchoParams {
message: string;
}
const webController: webview.WebviewController =
new webview.WebviewController();
function createResponse(
id: string,
ok: boolean,
pending: boolean,
data: string,
error: string
): BridgeResponse {
return {
id,
ok,
pending,
data,
error
};
}
function callbackToH5(response: BridgeResponse): void {
const responseJson = JSON.stringify(response);
// 再 stringify 一次,把 JSON 文本安全地变成 JS 字符串字面量。
const script =
`window.HarmonyBridge.__resolve(${JSON.stringify(responseJson)})`;
webController.runJavaScript(script)
.catch((error: BusinessError) => {
console.error(
`runJavaScript failed, code=${error.code}, message=${error.message}`
);
});
}
class NativeBridge {
invoke(requestJson: string): string {
try {
const request = JSON.parse(requestJson) as BridgeRequest;
switch (request.method) {
case 'sum': {
const params = JSON.parse(request.params) as SumParams;
const result = params.a + params.b;
return JSON.stringify(
createResponse(
request.id,
true,
false,
result.toString(),
''
)
);
}
case 'delayEcho': {
const params = JSON.parse(request.params) as EchoParams;
setTimeout(() => {
callbackToH5(
createResponse(
request.id,
true,
false,
`ArkTS received: ${params.message}`,
''
)
);
}, 500);
return JSON.stringify(
createResponse(
request.id,
true,
true,
'',
''
)
);
}
default:
return JSON.stringify(
createResponse(
request.id,
false,
false,
'',
`Unknown method: ${request.method}`
)
);
}
} catch (error) {
return JSON.stringify(
createResponse(
'',
false,
false,
'',
`Invalid bridge request: ${String(error)}`
)
);
}
}
}
const nativeBridge = new NativeBridge();
这里真正需要关注的不是 switch,而是协议。
同步调用直接返回完整 BridgeResponse;异步调用先返回:
{
"pending": true
}
等业务结束后,再通过 runJavaScript() 把最终结果推给网页。
这样同步和异步能力可以共用一个入口。
五、把 Bridge 注入 Web 页面
页面部分保持很小:
@Entry
@Component
struct Index {
aboutToDisappear(): void {
try {
webController.deleteJavaScriptRegister('NativeBridge');
} catch (error) {
const err = error as BusinessError;
console.error(
`deleteJavaScriptRegister failed: ${err.code}, ${err.message}`
);
}
}
build() {
Column() {
Web({
src: $rawfile('bridge/index.html'),
controller: webController
})
.width('100%')
.height('100%')
.javaScriptAccess(true)
.javaScriptProxy({
object: nativeBridge,
name: 'NativeBridge',
methodList: ['invoke'],
asyncMethodList: [],
controller: webController,
permission:
'{"javascriptProxyPermission":{' +
'"urlPermissionList":[' +
'{"scheme":"resource",' +
'"host":"rawfile",' +
'"port":"",' +
'"path":""}' +
']}}'
});
}
.width('100%')
.height('100%');
}
}
这里没有把几十个 Native 方法直接暴露出去,只注册:
NativeBridge.invoke
同时给 JavaScriptProxy 配置了 URL 权限范围,只允许:
resource://rawfile
这一类本地资源来源调用 Bridge。
官方 JavaScriptProxy 示例同样提供了 javascriptProxyPermission、urlPermissionList 以及 scheme、host、port、path 等粒度的限制方式;在官方 H5 适配指导中,也可以看到 registerJavaScriptProxy() 配合 URL 权限范围的用法。
六、H5 侧把调用包装成 Promise
接下来处理网页。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
<title>ArkWeb Bridge Demo</title>
</head>
<body>
<button onclick="testSum()">同步调用</button>
<button onclick="testAsync()">异步调用</button>
<pre id="result"></pre>
<script>
const pendingCalls = new Map();
let requestSeed = 0;
window.HarmonyBridge = {
call(method, params = {}) {
return new Promise((resolve, reject) => {
if (!window.NativeBridge ||
typeof window.NativeBridge.invoke !== 'function') {
reject(new Error('NativeBridge is not available'));
return;
}
const id = `req_${Date.now()}_${++requestSeed}`;
pendingCalls.set(id, {
resolve,
reject
});
const request = {
id,
method,
params: JSON.stringify(params)
};
try {
const raw =
window.NativeBridge.invoke(JSON.stringify(request));
const response = JSON.parse(raw);
// pending=true 表示 ArkTS 稍后主动回调。
if (response.pending) {
return;
}
pendingCalls.delete(id);
if (response.ok) {
resolve(response.data);
} else {
reject(new Error(response.error));
}
} catch (error) {
pendingCalls.delete(id);
reject(error);
}
});
},
__resolve(responseJson) {
const response = JSON.parse(responseJson);
const pending = pendingCalls.get(response.id);
if (!pending) {
return;
}
pendingCalls.delete(response.id);
if (response.ok) {
pending.resolve(response.data);
} else {
pending.reject(new Error(response.error));
}
}
};
async function testSum() {
try {
const result =
await HarmonyBridge.call('sum', {
a: 10,
b: 20
});
document.getElementById('result').textContent =
`sum result: ${result}`;
} catch (error) {
document.getElementById('result').textContent =
String(error);
}
}
async function testAsync() {
try {
const result =
await HarmonyBridge.call('delayEcho', {
message: 'Hello ArkTS'
});
document.getElementById('result').textContent =
result;
} catch (error) {
document.getElementById('result').textContent =
String(error);
}
}
</script>
</body>
</html>
到这里,H5 已经不需要知道 runJavaScript()、WebviewController 或 ArkTS 类是什么。
业务页面只认识:
await HarmonyBridge.call(method, params);
这正是统一 Bridge 最有价值的地方:平台通信细节被压到桥接层,业务代码只处理方法、参数和结果。
七、ArkTS 调 H5,为什么不能只靠字符串拼接
ArkTS 回调网页的关键代码是:
webController.runJavaScript(script);
官方说明中,runJavaScript() 会在当前页面上下文异步执行 JavaScript;如果需要获得更丰富的 JavaScript 返回类型,ArkWeb 还提供 runJavaScriptExt() 和对应的 JsMessageExt。当前官方 API 文档中,JsMessageExt 可以区分字符串、数值、布尔值、ArrayBuffer、数组等结果类型。
不过 Bridge 回调还有另一个问题:数据不能直接裸拼到 JavaScript 源码中。
例如不要这样写:
const script =
`window.onResult('${message}')`;
如果 message 本身包含引号、换行甚至 JavaScript 片段,最终生成的脚本可能改变原来的语义。
示例采用:
JSON.stringify(responseJson)
先把参数编码成合法的 JavaScript 字符串字面量,再拼入要执行的函数调用。
实际项目里,这一步很容易因为“正常中文字符串都能工作”而被忽略。
八、返回结果和异步调用怎么设计
同步调用比较直接:
H5 invoke()
↓
ArkTS 执行
↓
return JSON
↓
H5 Promise resolve
异步调用不能假设 ArkTS 的业务会立即结束。
所以这里增加了 requestId:
req_172...
流程变成:
H5 创建 requestId
↓
pendingCalls 保存 Promise
↓
NativeBridge.invoke()
↓
ArkTS 返回 pending=true
↓
ArkTS 异步任务执行
↓
runJavaScript()
↓
HarmonyBridge.__resolve()
↓
按 requestId 找到 Promise
↓
resolve / reject
这套结构还解决了并发问题。
如果同时发出三个请求:
req_1
req_2
req_3
即使返回顺序变成:
req_3
req_1
req_2
H5 也能根据 ID 找回对应的 Promise,而不是依赖“谁先请求谁先返回”。
如果业务更适合持续、高频的数据交换,而不是 RPC 式的一问一答,则可以评估 ArkWeb 官方提供的 WebMessagePort。官方文档明确给出了 createWebMessagePorts()、postMessage()、postMessageEvent() 和 onMessageEvent() 建立双向数据通道的方案;WebMessage 支持 string 和 ArrayBuffer,对象数据可以先通过 JSON 序列化为 string。
所以不要把所有 Web 与 Native 通信都强行塞进一种 Bridge。
九、不可信网页为什么不能随意暴露 Native 方法
JS Bridge 最需要警惕的地方其实不是参数类型,而是能力边界。
一旦把:
NativeBridge
注入网页,这个对象就不再只是 ArkTS 内部代码。
网页脚本可以调用其中被允许的方法。
如果同一个 Web 组件既加载自己的本地页面,又可能跳转到外部网页,却给所有来源暴露诸如:
getToken
readUserData
openNativePage
deleteFile
pay
这样的能力,Bridge 就会从通信接口变成攻击面。
因此至少要把三件事做好:
第一,只暴露必要方法。
本文只注册:
methodList: ['invoke']
业务能力再在 invoke() 内部做白名单分发,而不是把整个对象的方法都开放给网页。
第二,限制允许调用 Bridge 的页面来源。
本文通过:
{
"scheme": "resource",
"host": "rawfile"
}
限制到应用内本地资源来源。
如果以后切换到 HTTPS 在线页面,应根据实际业务域名配置对应的 JavaScriptProxy URL 权限,而不是为了“省事”把来源范围无限放大。
第三,Bridge 内部仍然要校验 method 和参数。
网页传来:
{
"method": "anything"
}
不能直接通过反射或动态属性访问执行任意 Native 方法。
本文采用明确的:
switch (request.method)
未知方法直接返回错误:
Unknown method
Bridge 应该被当作应用对 Web 开放的一组 API,而不是 ArkTS 世界的一扇后门。
十、几个容易理解错的地方
1. runJavaScript() 不等于“调用固定 H5 API”
它本质上执行的是 JavaScript 脚本。
所以:
runJavaScript('htmlTest()')
可以调用网页函数,也可以执行其他合法 JavaScript。
官方 FAQ 也采用在 onPageEnd 后通过 runJavaScript() 操作页面 DOM 的方式说明这一能力。
2. Bridge 可用时机和页面加载时机不是一回事
WebviewController 必须和 Web 组件建立关联后,实例方法才能正常工作。
而网页中的函数又必须已经进入当前页面上下文。
因此如果 ArkTS 一创建页面就立即:
runJavaScript('window.xxx()')
但 H5 还没有定义 window.xxx,自然无法得到预期结果。
需要应用启动后主动向 H5 推送初始化数据时,可以结合 Web 页面生命周期设计初始化时机,而不是用固定延时“猜”页面什么时候准备好。
3. 对象参数最好建立自己的协议
不要让 Bridge 一会儿传对象、一会儿传数组、一会儿传多个位置参数。
统一成 JSON 协议后:
id
method
params
日志、错误处理、版本升级都会简单很多。
4. Bridge 不再使用时要解除注册
JavaScriptProxy 不应该只注册不释放。
页面或 Bridge 生命周期结束后,应结合页面结构调用 deleteJavaScriptRegister() 解除已经注册的对象。
十一、实际项目中怎么排查
如果 H5 调 ArkTS 没反应,可以按下面顺序查:
- 先确认组件。 当前页面到底使用的是应用 ArkWeb
Web,还是元服务AtomicServiceEnhancedWeb。后者目前不能直接套用本文的runJavaScript()/registerJavaScriptProxy()方案。 - 再确认版本。 HarmonyOS 7 对应 API 26.0.0,项目 SDK、设备系统版本和实际调用接口要对应。
- 检查 JavaScript 是否开启以及 Bridge 是否完成注册。 H5 可以先打印
window.NativeBridge,确认对象是否存在。 - 检查来源权限。 如果配置了
javascriptProxyPermission,当前网页的 scheme、host、port、path 必须落在允许范围内。 - 检查方法名。
methodList中没有暴露的方法不能按已注册 Bridge 方法使用。 - 检查参数协议。 JSON 是否能正常解析,字段类型是否符合约定。
- 检查回调时机。 ArkTS 调用 H5 函数时,该函数是否已经在当前 document 中定义。
- 最后看 requestId。 异步回调到达 H5 后,
pendingCalls中是否仍存在对应 ID。
这套顺序比一上来怀疑 ArkWeb 内核更容易缩小问题范围。
十二、什么时候改用 WebMessagePort
JavaScriptProxy + runJavaScript() 很适合“调用一个能力,等待一个结果”的 RPC 风格。
例如:
getUserInfo
openNativePage
chooseFile
queryConfig
startNativeTask
如果需求变成持续交换数据:
Native 连续推送状态
H5 高频发送消息
双方长期保持通信通道
就应该评估 WebMessagePort。
官方的数据通道方案是由应用侧创建两个消息端口,把其中一个通过 postMessage() 交给前端页面,双方随后分别持有端口进行通信,并在不再使用或 Webview 销毁前关闭端口。
也就是说,Bridge 的设计重点不是“找到唯一正确的 API”,而是先判断自己的通信模型。
开发经验总结
这套最小实践真正值得留下来的不是 sum() 或 delayEcho(),而是五个设计点。
一是把双向通信拆清楚。 H5 调 ArkTS 由 JavaScriptProxy 建立入口;ArkTS 主动回调 H5,可以使用 runJavaScript()。
二是不要让业务层直接依赖 ArkWeb。 用统一的:
HarmonyBridge.call(method, params)
把平台差异收口。
三是异步调用一定要有 requestId。 只要存在并发请求,就不能依赖调用顺序匹配返回结果。
四是 Bridge 本身就是安全边界。 暴露的方法越少越好,来源范围越明确越好,Native 侧仍然需要验证 method 和参数。
五是根据通信模型选机制。 一次请求一次返回适合 JS Bridge;持续双向消息可以继续评估官方 WebMessagePort 数据通道。
HarmonyOS 7 已正式进入 API 26 开发阶段,版本升级时除了关注新增 API,也应该重新检查这类跨运行环境接口的权限边界和生命周期。官方升级指南明确建议应用结合 API 变化进行适配评估。
如果项目里已经有一套 Android/iOS WebView Bridge,也可以进一步思考一个问题:**业务层协议能不能保持不变,只把 HarmonyOS ArkWeb 的 JavaScriptProxy 和 runJavaScript() 封装成新的平台适配层?**做到这一点之后,混合页面真正需要维护的就不再是三套 Bridge,而是一套协议、多个平台实现。
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐


所有评论(0)