《HarmonyOS技术精讲-ArkWeb》桥接两岸:JSBridge原生与Web互调
《HarmonyOS技术精讲-ArkWeb》桥接两岸:JSBridge原生与Web互调

开篇:一个容易被低估的通信问题
HarmonyOS NEXT 开发中,涉及到 Web 混合应用的场景越来越多。很多人第一次接触 ArkWeb 的 JSBridge 时,会发现官方示例能跑,但实际项目里数据传递总是丢、回调不执行、页面一销毁就报错。
这个问题本身不难理解——原生端和 Web 端是两个独立的运行环境,通信需要桥接。但真正麻烦的是:生命周期怎么对齐?异步回调怎么处理?Web 页面销毁后,原生端的回调怎么避免野指针?
这篇文章就从零搭一个完整的 JSBridge 通信示例,把传参、回调、返回值处理这些细节拆开讲清楚。
JSBridge 解决了什么问题
在 ArkWeb 里,原生(ArkTS)和 Web(JavaScript)各自有独立的执行上下文。如果想让 Web 页面调用原生能力(比如弹窗、获取设备信息),或者原生端主动调用 Web 页面里的 JS 函数,就需要一个桥接机制。
ArkWeb 提供了三个核心接口来完成这件事:
| 接口 | 方向 | 说明 |
|---|---|---|
runJavaScript |
原生 → Web | 原生端主动调用 Web 里的 JS 函数 |
registerJavaScriptProxy |
Web → 原生 | 把原生对象注册到 Web 上下文,供 JS 调用 |
callJavaMethod |
Web → 原生(带 Promise) | 模拟异步回调,让 JS 可以等待原生返回结果 |
这三个接口组合起来,就能实现双向通信。但实际项目中,生命周期管理和异步回调处理才是真正的难点,接口本身反而不复杂。
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(建议真机测试,模拟器部分行为不一致)
核心实现:双向通信的完整示例
第一步:原生调用 Web(runJavaScript)
这个场景很常见:原生端需要触发 Web 页面的某个 JS 函数,比如通知页面刷新数据。
原生端(ArkTS)
// MainAbility/Index.ets
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct Index {
private controller: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
// 加载本地 H5 页面,或者远程 URL
Web({ src: $rawfile('index.html'), controller: this.controller })
.width('100%')
.height('80%')
Button('原生调用 JS')
.onClick(() => {
// 通过 runJavaScript 执行 JS 代码
this.controller.runJavaScript('updateMessage("来自原生的消息")');
})
.width('80%')
.margin({ top: 10 })
}
}
}
这里的关键点在于:runJavaScript 是异步的,但返回值只能是一个字符串。如果 JS 函数返回的是对象或数字,都会自动转成字符串。所以建议 JS 端直接返回 JSON 字符串,原生端再解析。
Web 端(index.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ArkWeb Demo</title>
<script>
// 被原生调用的 JS 函数
function updateMessage(msg) {
document.getElementById('message').innerText = msg;
// 如果需要返回值给原生
return JSON.stringify({ code: 0, data: msg });
}
</script>
</head>
<body>
<h1>JSBridge 测试</h1>
<p id="message">等待原生消息...</p>
</body>
</html>
注意: runJavaScript 的执行是顺序的,但如果页面还没加载完成就调用,会直接报错。需要在 onPageEnd 回调里确认页面加载状态后再调用。
第二步:Web 调用原生(registerJavaScriptProxy)
这个场景更复杂一些:Web 页面需要调用原生能力,比如弹一个 Toast、获取设备信息、读取本地文件。
原生端暴露对象
需要定义一个类,里面包含供 JS 调用的方法。
// JsBridgeObject.ets
export class JsBridgeObject {
// 方法名和参数必须与 JS 端约定一致
showToast(message: string) {
// 注意:这里的 this 指向有问题,后面会讲解决方案
promptAction.showToast({ message: message });
}
// 返回 Promise 给 JS 使用
async getDeviceInfo(): Promise<string> {
const info = {
model: deviceInfo.model,
osVersion: deviceInfo.osFullName
};
return JSON.stringify(info);
}
}
注册到 Web 上下文
// MainAbility/Index.ets
import { webview } from '@kit.ArkWeb';
import { JsBridgeObject } from './JsBridgeObject';
@Entry
@Component
struct Index {
private controller: webview.WebviewController = new webview.WebviewController();
private bridge: JsBridgeObject = new JsBridgeObject();
build() {
Column() {
Web({ src: $rawfile('index.html'), controller: this.controller })
.width('100%')
.height('80%')
.onPageEnd(() => {
// 页面加载完成后注册 JS 代理
this.controller.registerJavaScriptProxy({
object: this.bridge,
name: 'nativeBridge',
methodList: ['showToast', 'getDeviceInfo'],
controller: this.controller
});
})
}
}
}
注意: registerJavaScriptProxy 必须在 onPageEnd 回调之后调用,否则注册无效。而且注册完成后,还需要调用 refresh() 才能立即生效。
Web 端调用
<script>
// 调用原生的 showToast
function callNativeToast() {
nativeBridge.showToast('来自 H5 的提示');
}
</script>
<button onclick="callNativeToast()">调用原生 Toast</button>
第三步:异步回调处理(callJavaMethod 模拟 Promise)
registerJavaScriptProxy 注册的方法默认是同步的。但如果原生方法需要耗时操作(比如读取文件、请求权限),JS 端不能卡住。这时候就需要用 callJavaMethod 来实现异步回调。
ArkWeb 提供了一个更优雅的方式:原生方法返回一个 Promise,JS 端就可以用 await 等待结果。
原生端返回 Promise
// JsBridgeObject.ets
import { deviceInfo } from '@kit.BasicServicesKit';
export class JsBridgeObject {
// 返回 Promise
async getDeviceInfo(): Promise<string> {
// 模拟 500ms 耗时操作
await new Promise(resolve => setTimeout(resolve, 500));
return JSON.stringify({
model: deviceInfo.deviceInfo.model,
osVersion: deviceInfo.getDeviceType()
});
}
}
Web 端调用
<script>
async function loadDeviceInfo() {
try {
const raw = await nativeBridge.getDeviceInfo();
const info = JSON.parse(raw);
document.getElementById('deviceInfo').innerText = `型号: ${info.model}, 系统: ${info.osVersion}`;
} catch (e) {
console.error('获取设备信息失败:', e);
}
}
</script>
关键点: 原生方法返回 Promise 后,ArkWeb 内部会转换成 JS 的 Promise,JS 端就可以直接用 await 或 .then() 接收。这种方式比手动传回调函数要稳定得多。
常见问题 1:生命周期引发的野指针异常
现象: 页面返回后,Web 页面已经销毁,但原生端发起的异步回调还在执行,这时候访问 WebviewController 会报 Cannot read properties of null。
原因: 页面退出时,ArkWeb 的 WebviewController 会被释放。但之前通过 runJavaScript 发起的异步操作,或者 registerJavaScriptProxy 注册的方法,如果回调中继续操作 controller,就会出现空指针。
解决方案: 在页面销毁前,手动清除引用,并标记页面状态。
// MainAbility/Index.ets
@Entry
@Component
struct Index {
private controller: webview.WebviewController = new webview.WebviewController();
private isPageActive: boolean = true;
build() {
Column() {
Web({ src: $rawfile('index.html'), controller: this.controller })
.width('100%')
.height('80%')
.onDisAppear(() => {
// 页面不可见时标记
this.isPageActive = false;
})
}
}
// 调用 JS 时先检查状态
callJsWhenActive() {
if (!this.isPageActive) {
console.warn('页面已销毁,跳过 JS 调用');
return;
}
this.controller.runJavaScript('updateMessage("test")');
}
}
常见问题 2:JSBridge 对象中的 this 指针丢失
现象: 在 JsBridgeObject 的 showToast 方法中,this 指向 undefined,导致无法调用 promptAction。
原因: registerJavaScriptProxy 内部会重新绑定 this,导致原本的实例方法丢失了上下文。
解决方案: 不要使用箭头函数(箭头函数不绑定 this),而是用普通函数,并在构造函数中手动绑定。
export class JsBridgeObject {
private callback: Function;
constructor() {
// 手动绑定 this
this.showToast = this.showToast.bind(this);
this.getDeviceInfo = this.getDeviceInfo.bind(this);
}
showToast(message: string) {
// 现在 this 指向实例
promptAction.showToast({ message: message });
}
}
如果觉得手动绑定麻烦,也可以用箭头函数定义方法:
export class JsBridgeObject {
// 箭头函数自动绑定定义时的 this
showToast = (message: string) => {
promptAction.showToast({ message: message });
}
}
最佳实践
1. 约定接口协议,避免硬编码
在原生和 Web 之间通信,推荐用一套固定的协议格式,比如所有返回都包一层 { code, data, message }。这样不管是成功还是失败,JS 端都能统一处理,避免原生返回不同类型时 Web 端解析出错。
2. 不要频繁调用 runJavaScript
每次 runJavaScript 都会创建一次 JS 执行环境。如果在循环或高频事件(比如 onScroll)中频繁调用,会导致 Web 页面卡顿。建议把需要多次调用的 JS 函数注册为全局函数,一次性调用。
3. 注册时机必须固定在 onPageEnd
registerJavaScriptProxy 如果写在 onPageBegin 或者其他早期回调里,很可能注册失败。只有 onPageEnd 保证页面 DOM 加载完毕,JS 环境可用。而且注册后必须调用一次 refresh(),否则需要等待下一次页面刷新才能生效。
Demo 入口
// EntryAbility/Index.ets
import { webview } from '@kit.ArkWeb';
import { JsBridgeObject } from '../common/JsBridgeObject';
@Entry
@Component
struct Index {
private controller: webview.WebviewController = new webview.WebviewController();
private bridge: JsBridgeObject = new JsBridgeObject();
build() {
Column() {
Button('原生调用 JS')
.onClick(() => {
this.controller.runJavaScript('updateMessage("原生消息")');
})
Web({ src: $rawfile('index.html'), controller: this.controller })
.width('100%')
.height('70%')
.onPageEnd(() => {
this.controller.registerJavaScriptProxy({
object: this.bridge,
name: 'nativeBridge',
methodList: ['showToast', 'getDeviceInfo'],
controller: this.controller
});
this.controller.refresh();
})
.onDisAppear(() => {
// 清理
})
}
}
}
示例代码地址:项目地址
FAQ
Q:为什么页面返回后,Web 调用原生方法没有反应?
A:页面销毁时,registerJavaScriptProxy 注册的代理会被自动移除。如果希望保持通信,需要在页面再次加载时重新注册,或者在单页面应用模式下保持 Web 页面存活。
Q:runJavaScript 第一次调用总是失败,第二次才成功?
A:检查一下调用时机。runJavaScript 必须在页面加载完成(onPageEnd)之后调用。如果页面还没加载完,JS 执行环境不存在,调用会静默失败。
Q:为什么模拟器上 JSBridge 不生效,真机正常?
A:模拟器的 Web 引擎版本可能与真机不一致,部分 JSBridge 行为存在差异。建议所有 JSBridge 相关的测试都在真机上完成,模拟器只用于 UI 布局验证。
更多推荐



所有评论(0)