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