在 HarmonyOS 的 ArkUI 开发中,Web 组件是实现混合开发(Hybrid)的核心。通过 Web 组件,开发者可以在应用内无缝嵌入网页,并通过 JSBridge 实现原生端(ArkTS)与前端(H5)的双向通信。
以下是 Web 组件集成与 JS 交互:
一、 Web 组件的四种加载方式
Web 组件支持多种数据来源,以适应不同的业务场景:
- 加载在线 URL:
Web({ src: 'https://...' }),适用于加载网络页面。注意需在module.json5中申请ohos.permission.INTERNET权限。 - 加载本地 rawfile:
Web({ src: $rawfile('index.html') }),适用于加载打包在src/main/resources/rawfile/目录下的本地 HTML 文件。 - resource 协议:
Web({ src: 'resource://rawfile/index.html' }),适用于运行时动态指定本地文件路径。 - loadData 渲染字符串:通过
controller.loadData(htmlStr, ...)直接渲染 HTML 字符串,适用于加载富文本数据。
import { webview } from '@kit.ArkWeb'; import { BusinessError } from '@kit.BasicServicesKit'; @Entry @Component struct WebLoadDemo { controller: webview.WebviewController = new webview.WebviewController(); // 用于 loadData 渲染的 HTML 字符串 @State htmlContent: string = '<!DOCTYPE html>' + '<html><head><title>动态内容</title></head>' + '<body>' + '<h1 style="color: #007AFF;">这是动态生成的内容</h1>' + '<p>无需任何文件,直接渲染字符串</p>' + '</body></html>'; build() { Column() { Text('Web 组件四种加载方式演示') .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ bottom: 10 }) Row({ space: 10 }) { // 方式一:加载在线 URL Button('加载在线URL') .onClick(() => { try { this.controller.loadUrl('https://developer.huawei.com/'); } catch (error) { console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); } }) // 方式二:加载本地 rawfile Button('加载本地Rawfile') .onClick(() => { try { this.controller.loadUrl($rawfile('index.html')); } catch (error) { console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); } }) // 方式三:使用 resource 协议 Button('Resource协议') .onClick(() => { try { this.controller.loadUrl('resource://rawfile/index.html'); } catch (error) { console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); } }) // 方式四:loadData 渲染字符串 Button('LoadData渲染') .onClick(() => { try { this.controller.loadData( this.htmlContent, 'text/html', // mimeType 'UTF-8' // encoding ); } catch (error) { console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); } }) } .margin({ bottom: 10 }) // Web 组件初始化(默认加载本地页面) Web({ src: $rawfile('index.html'), controller: this.controller }) .width('100%') .layoutWeight(1) .javaScriptAccess(true) .domStorageAccess(true) } .width('100%') .height('100%') .padding(20) } }代码要点解析:
加载在线 URL:点击按钮后通过
controller.loadUrl()加载华为开发者官网。注意需在module.json5中添加网络权限:"requestPermissions": [{ "name": "ohos.permission.INTERNET" }]加载本地 rawfile:使用
$rawfile('index.html')静态引用,指向src/main/resources/rawfile/index.html。该方式支持在 HTML 中正常引用同目录下的 CSS、JS、图片等资源。resource 协议:使用
resource://rawfile/index.html字符串格式,与$rawfile()指向同一目录,但它是纯字符串,可以在运行时动态拼接和构造路径。loadData 渲染字符串:无需任何文件,直接将 HTML 内容作为字符串传入
controller.loadData()进行渲染。适合做富文本邮件、协议弹窗、动态内容展示等场景。
二、 ArkUI 与 Web 的双向通信机制
双向通信是 Hybrid 开发的基石,主要分为“正向调用”和“反向注入”两个方向:
1. 正向通信:ArkUI 调用 Web (runJavaScript)
原生端通过WebviewController的runJavaScript方法,可以直接执行网页中的 JS 代码,并支持通过回调获取返回值。
this.controller.runJavaScript('webFunction("Hello from ArkUI!")', (error, result) => { if (!error) { console.info('Web返回数据:' + result); } });注意:该方法必须在页面加载完成后调用(如在onPageEnd回调中),否则可能执行失败。
2. 反向通信:Web 调用 ArkUI (JavaScriptProxy)
这是最核心的 JSBridge 实现方式。通过javaScriptProxy将 ArkUI 对象注入到 Webview 的window对象中,前端即可像调用本地方法一样调用原生能力。
ArkTS 侧代码:
// 1. 定义原生对象 class NativeObj { makePhoneCall(number: string): void { console.info('准备拨打电话:' + number); } } // 2. 在 Web 组件中注入 Web({ src: $rawfile('index.html'), controller: this.controller }) .javaScriptProxy({ object: new NativeObj(), name: "native", // 前端通过 window.native 访问 methodList: ["makePhoneCall"], // 允许调用的方法白名单 controller: this.controller })Web (JS) 侧调用:
window.native.makePhoneCall('10086');三、 动态注册与注销
除了初始化时静态注入,ArkUI 还支持在运行时动态管理注入对象:
- 动态注册:使用
controller.registerJavaScriptProxy(obj, name, methods)。 - 刷新生效:动态注册后,必须执行
controller.refresh()刷新页面,注入才会生效! - 注销对象:使用
controller.deleteJavaScriptRegister(name)移除已注入的对象。
ArkTS 侧代码
import { webview } from '@kit.ArkWeb'; import { BusinessError } from '@kit.BasicServicesKit'; // 1. 定义需要注入到前端的原生对象 class NativeBridge { sayHello(): string { return 'Hello from ArkTS Dynamic Proxy!'; } } @Entry @Component struct DynamicProxyDemo { controller: webview.WebviewController = new webview.WebviewController(); @State bridgeObj: NativeBridge = new NativeBridge(); build() { Column() { Row({ space: 10 }) { // 2. 动态注册按钮 Button('动态注册') .onClick(() => { try { this.controller.registerJavaScriptProxy( this.bridgeObj, 'nativeBridge', // 前端访问的对象名 ['sayHello'] // 允许调用的方法白名单 ); // ⚠️ 核心步骤:动态注册后必须刷新页面才能生效! this.controller.refresh(); } catch (error) { console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); } }) // 3. 注销对象按钮 Button('注销对象') .onClick(() => { try { this.controller.deleteJavaScriptRegister('nativeBridge'); } catch (error) { console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); } }) } .margin({ bottom: 10 }) // 4. Web 组件 Web({ src: $rawfile('index.html'), controller: this.controller }) .width('100%') .layoutWeight(1) .javaScriptAccess(true) } .width('100%') .height('100%') .padding(20) } }Web (JS) 侧代码 (index.html)
<!DOCTYPE html> <html> <head> <title>Dynamic Proxy Test</title> </head> <body> <button onclick="callNative()">调用原生方法</button> <p id="result"></p> <script> function callNative() { // 检查对象是否已注册 if (window.nativeBridge) { const msg = window.nativeBridge.sayHello(); document.getElementById('result').innerText = msg; } else { document.getElementById('result').innerText = '原生对象未注册!'; } } </script> </body> </html>代码要点解析:
- 动态注册时机:与初始化时使用的
.javaScriptProxy()不同,registerJavaScriptProxy可以在任意运行时阶段调用(例如在onPageEnd回调中,或根据用户登录状态动态注入)。 - 强制刷新:动态注入的对象不会自动同步到当前已加载的页面上下文中,必须紧接着调用
controller.refresh()重新加载页面,注入才会真正生效。 - 内存管理与注销:当 H5 页面不再需要调用原生能力,或者组件即将销毁时,务必调用
deleteJavaScriptRegister移除注入的对象。这能有效断开 ArkTS 对象与 WebView 之间的引用链,防止内存泄漏。
四、 进阶:JSBridge 分层架构设计
在实际的企业级 Hybrid 应用中,建议不要将业务逻辑直接写在JavaScriptProxy中,而是采用分层设计以提高通用性和灵活性:
- 通信层:负责 ArkTS 与 JS 之间的数据传递,屏蔽底层通信机制。通常将数据序列化为 JSON 字符串进行传递。
- 通道层 (Channel):允许注册多种方法通道。JS 侧负责将 API 信息打包,ArkTS 侧负责解包并分发给具体的处理方法,处理完毕后再通过
runJavaScript执行 JS 侧的回调函数。 - 方法层:具体的业务 API 实现(如获取设备信息、拉起支付、路由跳转等)。
这种架构类似于小程序的底层通信规范,能够极大地降低原生端与前端代码的耦合度,便于后续的业务扩展与维护。
1. ArkTS 侧:通道层与通信层实现
import web_webview from '@ohos.web.webview'; // 1. 方法层:定义具体的业务 API class DeviceService { getInfo(): string { return JSON.stringify({ model: 'HarmonyOS Device', osVersion: '5.0' }); } } // 2. 通道层:负责解包、路由分发与回调 class ChannelManager { private deviceService: DeviceService = new DeviceService(); // 处理来自 H5 的调用 call(channelType: string, objectJson: string): string { const params = JSON.parse(objectJson); let result: any = {}; if (channelType === 'device.getInfo') { result = this.deviceService.getInfo(); } else { result = { code: -1, message: 'API not found' }; } return JSON.stringify(result); } } // 3. 通信层:注入到 Web 中的代理对象 class BridgeProxy { private channelManager: ChannelManager = new ChannelManager(); // 这是唯一暴露给 JS 的通信方法 nativeMethod(channelType: string, objectJson: string): string { return this.channelManager.call(channelType, objectJson); } } @Entry @Component struct HybridPage { private webController: web_webview.WebviewController = new web_webview.WebviewController(); private bridgeProxy: BridgeProxy = new BridgeProxy(); build() { Column() { Web({ src: $rawfile('index.html'), controller: this.webController }) .javaScriptProxy({ object: this.bridgeProxy, name: 'JSBridge', // 注入到 window 的对象名 methodList: ['nativeMethod'], // 通信白名单,仅暴露通信层方法 controller: this.webController }) .width('100%') .height('100%') } } }2. Web (JS) 侧:通道层与通信层实现
<!DOCTYPE html> <html> <head><title>JSBridge Layered Demo</title></head> <body> <button onclick="callNativeApi()">获取设备信息</button> <p id="result"></p> <script> // 通道层:封装打包与解包逻辑 function nativeCall(channelType, params) { const objectJson = JSON.stringify(params); // 调用通信层注入的方法 const resultJson = window.JSBridge && window.JSBridge.nativeMethod(channelType, objectJson); return resultJson ? JSON.parse(resultJson) : null; } // 方法层:业务调用 function callNativeApi() { const res = nativeCall('device.getInfo', {}); document.getElementById('result').innerText = JSON.stringify(res); } </script> </body> </html>五、 安全权限管控(Permission 配置)
在真实业务中,将原生能力暴露给 H5 存在极大的安全风险。HarmonyOS 提供了细粒度的权限控制机制,允许在javaScriptProxy注入时通过 JSON 字符串限制访问来源。
核心逻辑:
权限配置分为对象级和方法级。对象级权限指定哪些 URL 可以访问该对象的所有方法;方法级权限则进一步细化,定义特定 URL 对特定方法的访问权。
.javaScriptProxy({ object: this.nativeObj, name: "native", methodList: ["makePhoneCall", "getUserInfo"], controller: this.controller, // 安全权限配置 permission: `{ "javascriptProxyPermission": { "urlPermissionList": [ { "scheme": "https", "host": "trusted-domain.com", "port": "", "path": "" } ], "methodList": [ { "methodName": "makePhoneCall", "urlPermissionList": [ { "scheme": "https", "host": "trusted-domain.com", "port": "", "path": "" }, { "scheme": "resource", "host": "rawfile", "port": "", "path": "" } ] } ] } }` })安全建议:务必严格校验event.origin或来源 URL,避免动态拼接 JS 字符串,防止 XSS 注入攻击。
六、 高级通信通道:WebMessagePort
对于高频交互、大数据量传输或复杂的实时业务(如 IM 聊天、实时音视频状态同步),传统的runJavaScript和JavaScriptProxy可能面临性能瓶颈。此时推荐使用基于 HTML5 标准的WebMessagePort机制。
核心逻辑:
- 原生侧调用
createWebMessagePorts()创建一对消息端口[port1, port2]。 - 将
port2通过postMessage发送给 H5,H5 接收后缓存该端口并监听onmessage事件。 - 双方后续均通过
port.postMessage()进行双向异步通信,无需反复注入对象或执行 JS 脚本。
// ArkTS 侧创建并分发端口 Button('创建数据通道') .onClick(() => { const ports = this.controller.createWebMessagePorts(); // 将 port[1] 传递给 H5 this.controller.postMessage('init_port', [ports[1]]); // 监听来自 H5 的消息 ports[0].onMessageEvent = (message: webview.WebMessage) => { console.info('收到H5消息: ' + message.data); }; })七、 渲染性能优化:同层渲染与组件鸿蒙化
在 Hybrid 应用中,如果 H5 内嵌了大量的原生交互组件(如视频播放器、地图、复杂表单),传统的 WebView 渲染会导致严重的性能损耗和交互割裂感。
核心优化策略:
- 同层渲染:利用 ArkWeb 提供的同层渲染能力,将部分 H5 节点替换为原生 ArkUI 组件。原生组件直接嵌入到 WebView 的渲染树中,获得与原生应用一致的滑动跟手体验和渲染效率。
- API 鸿蒙化:针对 H5 侧强依赖的平台相关 API,提供一套完整的 HarmonyOS 版本实现(参考成熟的小程序框架规范),确保原有前端业务逻辑无需大幅改动即可无缝运行在鸿蒙环境中。
- 生命周期管理:确保所有的 JSBridge 通信都在 H5 页面加载完成(
onPageEnd)之后发起,避免调用未定义的函数或对象导致异常。同时,在页面销毁时及时注销代理对象,防止内存泄漏。
八、 架构分层:MVVM 模式下的 Web 容器设计
在大型应用中,直接在 UI 组件中处理复杂的 Web 交互逻辑会导致代码臃肿且难以维护。建议引入 MVVM 模式进行分层解耦:
- View 层(UI 组件):
Web组件仅负责渲染和展示,不包含任何业务逻辑。它通过@Link或@Prop接收来自 ViewModel 的状态(如 URL、加载进度),并通过事件回调通知 ViewModel 用户的交互(如页面开始加载、加载完成)。 - ViewModel 层(状态与逻辑):作为 Web 容器的核心管理者,ViewModel 持有
WebviewController实例,并管理页面状态(如加载进度、错误信息)。它处理所有业务逻辑,如权限校验、URL 拦截、错误重试等,并更新状态以驱动 UI 变化。 - Model 层(数据源):定义 Web 容器的数据结构和配置,可以来自本地配置或远程接口,实现 Web 容器的动态化配置。
九、 通信协议:标准化的 JSBridge 设计
直接使用runJavaScript和JavaScriptProxy进行通信,会导致代码耦合度高,难以维护。建议设计一套标准化的通信协议:
- 统一消息格式:定义一套统一的 JSON 消息格式,包含
action(操作类型)、params(参数)、callbackId(回调 ID)等字段。所有 ArkTS 与 H5 的通信都遵循此格式。 - 通道管理器(Channel Manager):在 ArkTS 侧实现一个通道管理器,负责接收 H5 发来的消息,根据
action字段分发给不同的业务模块处理。处理完成后,通过callbackId找到对应的回调函数,并将结果返回给 H5。 - H5 SDK 封装:在 H5 侧封装一个 JS SDK,提供统一的
invoke方法。H5 开发者只需调用invoke('actionName', params).then(...),SDK 内部负责消息的序列化、发送和回调管理,对 ArkTS 的通信细节完全透明。
十、 性能优化:渲染与交互体验
当 Web 页面内嵌大量原生交互组件时,传统的 WebView 渲染会导致严重的性能损耗和交互割裂感。
- 同层渲染(Peer Rendering):利用 ArkWeb 提供的同层渲染能力,将部分 H5 节点(如视频播放器、地图)替换为原生 ArkUI 组件。原生组件直接嵌入到 WebView 的渲染树中,获得与原生应用一致的滑动跟手体验和渲染效率。
- WebMessagePort 高频通信:对于高频交互、大数据量传输或复杂的实时业务(如 IM 聊天、实时音视频状态同步),传统的
runJavaScript和JavaScriptProxy可能面临性能瓶颈。此时推荐使用基于 HTML5 标准的WebMessagePort机制,建立双向异步通信通道,避免反复注入对象或执行 JS 脚本。 - 预加载与缓存策略:对于核心业务页面,可以在应用启动时预加载 Web 容器,并缓存静态资源。通过
WebviewController的缓存接口,配置合理的缓存策略,减少网络请求,提升页面加载速度。