HarmonyOS7 原生和网页怎么互调:WebJavaScriptBridge 双向通信入门
文章目录
前言
一旦 Web 组件不只是拿来“展示网页”,事情就开始复杂了。活动页想调用原生分享,H5 想拿登录态,原生按钮想通知网页切 tab,最后都会落到一个词上:桥接。
这个话题一上来就讲实现细节,往往很容易把人讲懵。因为桥接不是一个 API,而是一套通信关系。案例这一页的价值就在这,它没有急着写满接口,而是先把 ArkUI 和网页之间的调用方向讲清楚。
说白了,你得先知道谁调用谁,再谈怎么调用。

案例的结构重点
案例把桥接拆成了三部分:
- ArkUI 调 JavaScript
- JavaScript 调 ArkUI
- 双向通信的完整流程

同时,它还放了一个最小状态 jsResult,用来观察交互后的结果变化。这个思路是对的,因为桥接这种东西,如果没有一个状态出口,很难确认链路到底通没通。
完整代码

import { DEMO_BG_COLOR, DEMO_CARD_COLOR, DEMO_THEME_COLOR, DEMO_SUBTEXT_COLOR } from './types'
@Entry
@Component
struct WebJavaScriptBridge {
@State isShow: boolean = true
@State jsResult: string = '等待调用...'
callJsDemo(): void {
this.jsResult = `JS 调用结果: ${Date.now()}`
}
build() {
Column() {
if (this.isShow) {
Scroll() {
Column({ space: 16 }) {
Text('Web JavaScript 交互').fontSize(16).fontColor(DEMO_THEME_COLOR).fontWeight(FontWeight.Bold)
Column() {
Text('JS Bridge 交互模式').fontSize(13).fontColor('#4D96FF').margin({ bottom: 10 })
Column({ space: 12 }) {
Column() {
Text('方向一: ArkUI -> JS').fontSize(12).fontColor('#4D96FF').fontWeight(FontWeight.Bold)
Column() {
Text('controller.runJavaScript(code)').fontSize(11).fontColor('#FFFFFF')
}
.width('100%')
.height(40)
.backgroundColor('#4D96FF')
.borderRadius(6)
.justifyContent(FlexAlign.Center)
}
Column() {
Text('方向二: JS -> ArkUI').fontSize(12).fontColor('#FFA500').fontWeight(FontWeight.Bold)
Column() {
Text('.onControllerAttached() 注册 JS 回调').fontSize(11).fontColor('#FFFFFF')
}
.width('100%')
.height(40)
.backgroundColor('#FFA500')
.borderRadius(6)
.justifyContent(FlexAlign.Center)
}
Column() {
Text('双向通信流程').fontSize(12).fontColor('#6BCB77').fontWeight(FontWeight.Bold)
Column({ space: 4 }) {
Text('1. ArkUI 调用 runJavaScript() 执行 JS').fontSize(10).fontColor('#FFFFFFCC')
Text('2. JS 通过 window.ohosCallNative() 回调').fontSize(10).fontColor('#FFFFFFCC')
Text('3. 注册 javaScriptProxy 实现双向调用').fontSize(10).fontColor('#FFFFFFCC')
}
.width('100%')
.backgroundColor('#6BCB77')
.borderRadius(6)
.padding(8)
}
}
}
.backgroundColor(DEMO_CARD_COLOR)
.padding(12)
.borderRadius(12)
Column({ space: 8 }) {
Text('交互结果展示').fontSize(14).fontColor(DEMO_THEME_COLOR).fontWeight(FontWeight.Bold)
Text(this.jsResult).fontSize(13).fontColor('#1A1A1A')
Button('模拟 JS 调用')
.onClick(() => { this.callJsDemo() })
.fontSize(12)
.backgroundColor(DEMO_THEME_COLOR)
Button('模拟 runJavaScript')
.onClick(() => { this.jsResult = '执行 JS: changeTitle("New Title")' })
.fontSize(12)
.backgroundColor('#6BCB77')
}
.backgroundColor('#E8F4FD')
.padding(14)
.borderRadius(12)
Column({ space: 4 }) {
Text('安全注意事项').fontSize(13).fontColor('#FF6B6B')
Text('使用 javaScriptProxy 时注意安全').fontSize(12).fontColor('#333333')
Text('避免注入恶意代码和 XSS 攻击').fontSize(12).fontColor('#333333')
Text('验证所有 JS 回调的数据合法性').fontSize(12).fontColor('#333333')
}
.width('100%')
.backgroundColor('#FFF0F0')
.padding(12)
.borderRadius(8)
}
.width('100%')
}
}
Text('WebJavaScriptBridge - JS Bridge 通信')
.fontSize(12)
.fontColor(DEMO_SUBTEXT_COLOR)
.margin({ top: 12 })
}
.width('100%')
.height('100%')
.backgroundColor(DEMO_BG_COLOR)
.padding(16)
}
}
先把两条调用方向讲透
这部分是整篇最重要的认知点。
方向一:ArkUI 调 JavaScript
这是最容易理解的一条链路。原生侧通过控制器执行网页里的 JS 代码:
controller.runJavaScript(code)
比如网页里有个 changeTitle(),那原生按钮就可以主动触发它。这个能力特别适合什么场景?
很典型的一类,是原生操作区控制网页内容。比如原生头部点“切换主题”“打开弹层”“跳到某个 section”,页面主体其实是 H5,这时候就要用原生去调 JS。
方向二:JavaScript 调 ArkUI
反过来,网页也可能需要告诉原生侧一些事情。比如:
- H5 登录完成,通知原生刷新用户信息
- 点击网页里的分享按钮,唤起原生分享面板
- H5 页面里的某个链接不是普通跳转,而是触发原生路由
案例里用这句描述得比较抽象:
.onControllerAttached() 注册 JS 回调
真实项目里,核心思想就是把一个可调用的原生接口暴露给网页脚本。网页拿到之后,就能带着参数回调原生。
jsResult 这个状态为什么要留着
案例用 @State jsResult 保存交互结果:
@State jsResult: string = '等待调用...'
然后在点击按钮后更新它:
callJsDemo(): void {
this.jsResult = `JS 调用结果: ${Date.now()}`
}
这不是为了做花活,而是为了让桥接变得可观测。
桥接出问题时,最麻烦的是你经常不知道卡在哪一段。是按钮没点到?是原生方法没执行?还是网页没回传?只要你在页面里保留一个明确的结果显示区,调试效率会高很多。
双向通信流程可以怎么理解
案例把完整流程总结成三步:
1. ArkUI 调用 runJavaScript() 执行 JS
2. JS 通过 window.ohosCallNative() 回调
3. 注册 javaScriptProxy 实现双向调用
这三句本质上讲的是一件事:原生和网页都不能把对方当成本地函数直接调用,中间需要一个约定好的桥。
你可以把它理解成一套跨边界协议。
原生发消息过去,网页按约定的方法名和参数处理;网页再回消息回来,原生按同样约定接住。真正麻烦的不是调用本身,而是调用协议是否稳定。
关键代码怎么迁移到真实业务
如果你要落地到项目里,建议最开始只定义少量桥接动作,不要一口气开放十几个接口。
一个靠谱的做法是:
- 先约定统一的数据格式,比如
{ action, payload } - 让原生只暴露少数可控动作,比如分享、登录态获取、页面关闭
- 对所有来自网页的数据做校验,不要直接信任
桥接的核心不是“能调通”,而是“可控、可维护、可排查”。这点很重要。
安全提醒不是装饰
案例最后专门给了安全区,这一点非常对。
Text('使用 javaScriptProxy 时注意安全')
Text('避免注入恶意代码和 XSS 攻击')
Text('验证所有 JS 回调的数据合法性')
很多人做桥接只关注功能,结果把网页当成绝对可信。只要外部内容来源稍微复杂一点,这种思路就会出事。
至少要守住三条底线:
第一,不要执行未经约束的任意脚本。
第二,不要把敏感能力全部开放给网页。
第三,所有回调参数都要校验格式和内容,不能拿来就用。
这个案例适合的真实场景
活动页分享、H5 支付结果通知、内容页唤起原生评论区、嵌入式管理后台、Hybrid 登录流,这些都是桥接高频场景。
尤其当你项目里一部分页面是网页、一部分页面是原生时,桥接能力几乎绕不过去。区别只在于你是先把它设计清楚,还是后面边补边救火。
常见坑我提前说几个
第一个坑,是直接把方法名和参数写死在多个页面里。后面一改协议,所有地方一起炸。
第二个坑,是桥接动作没有日志。调试时只看到“没反应”,但不知道消息有没有发出去。
第三个坑,是把桥接当成万能方案。很多高频交互如果可以原生化,长期看还是原生更稳。
第四个坑,是没有设计调用失败的兜底。网页调原生失败、原生调网页失败,都应该有明确反馈。
写在最后
桥接这件事,最怕的不是复杂,而是含糊。只要你先把方向分清楚,再把协议和安全边界收紧,它其实就是一套可以逐步扩展的通信层。
这个案例最适合拿来当认知起点。你不一定今天就把完整桥接写完,但至少从现在开始,原生和网页之间那条线不会再是一团雾了。
更多推荐



所有评论(0)