前言

一旦 Web 组件不只是拿来“展示网页”,事情就开始复杂了。活动页想调用原生分享,H5 想拿登录态,原生按钮想通知网页切 tab,最后都会落到一个词上:桥接。

这个话题一上来就讲实现细节,往往很容易把人讲懵。因为桥接不是一个 API,而是一套通信关系。案例这一页的价值就在这,它没有急着写满接口,而是先把 ArkUI 和网页之间的调用方向讲清楚。

说白了,你得先知道谁调用谁,再谈怎么调用。

手绘笔记风技术信息图,主题是 HarmonyOS7 的 WebJavaScriptBridge 双向

案例的结构重点

案例把桥接拆成了三部分:

  • ArkUI 调 JavaScript
  • JavaScript 调 ArkUI
  • 双向通信的完整流程

手绘流程图,展示文章里的完整桥接流程:1. ArkUI 通过 controller.runJavaS

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

完整代码

手绘框架图,拆解 Web JavaScript Bridge 的结构重点:上方或左侧是 ArkUI

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 登录流,这些都是桥接高频场景。

尤其当你项目里一部分页面是网页、一部分页面是原生时,桥接能力几乎绕不过去。区别只在于你是先把它设计清楚,还是后面边补边救火。

常见坑我提前说几个

第一个坑,是直接把方法名和参数写死在多个页面里。后面一改协议,所有地方一起炸。

第二个坑,是桥接动作没有日志。调试时只看到“没反应”,但不知道消息有没有发出去。

第三个坑,是把桥接当成万能方案。很多高频交互如果可以原生化,长期看还是原生更稳。

第四个坑,是没有设计调用失败的兜底。网页调原生失败、原生调网页失败,都应该有明确反馈。

写在最后

桥接这件事,最怕的不是复杂,而是含糊。只要你先把方向分清楚,再把协议和安全边界收紧,它其实就是一套可以逐步扩展的通信层。

这个案例最适合拿来当认知起点。你不一定今天就把完整桥接写完,但至少从现在开始,原生和网页之间那条线不会再是一团雾了。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐