一、背景说明

混合开发是鸿蒙应用承接存量 Web 业务的主流方案:H5 页面负责快速迭代的业务界面,原生侧提供设备能力与性能敏感模块,两者之间靠 JSBridge 通信。ArkWeb 的 Chromium 内核在 26.0.0 版本已从 132 升级到 144,能力持续跟进,而应用侧通信入口始终是 @ohos.web.webview 的 WebviewController。本文完整实现"H5 调用原生方法(registerJavaScriptProxy)+ 原生调用 H5 函数(runJavaScript)"的双向 JSBridge,代码可直接运行。

二、技术原理

ArkWeb 提供两条通信通道,方向相反、配合使用:

  1. H5 → 原生(JavaScriptProxy):应用把一个 ArkTS 对象注入到页面的 window 上,H5 直接以 window.对象名.方法() 调用,参数与返回值支持 string / number / boolean 及可嵌套的 Dictionary、Array。注入方式有两种:Web 组件初始化时用 javaScriptProxy() 属性,或初始化后用 registerJavaScriptProxy()(需在下一次页面加载后生效);
  2. 原生 → H5(runJavaScript):在当前页面上下文异步执行任意 JS 脚本,通过 Promise 拿返回值,常用于把原生计算结果回推给页面。

数据流为:Web 组件绑定 WebviewController → onControllerAttached 时 registerJavaScriptProxy 注入桥对象 → 页面加载完成后 H5 调用 window.bridge.getTitle() → 原生执行后用 runJavaScript 回调 window.onNativeResult(data) 通知页面。

三、实现步骤

无需任何权限。工程要求 Stage 模型、API 12 及以上(含 26.0.0)。先准备本地测试页 src/main/resources/rawfile/index.html(完整代码):

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>JSBridge Demo</title></head>
<body>
  <h3 id="status">等待调用...</h3>
  <button onclick="callNative()">调用原生 getDeviceInfo</button>
  <script>
    // 原生侧通过 runJavaScript 回调此函数
    function onNativeResult(data) {
      document.getElementById('status').innerText = '原生返回:' + data;
    }
    // H5 主动调用注入的原生对象
    function callNative() {
      var r = window.bridge.getDeviceInfo();
      document.getElementById('status').innerText = 'H5 拿到:' + r;
    }
  </script>
</body>
</html>

新建 src/main/ets/pages/Index.ets(完整代码,双向通信):

import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';
import { deviceInfo } from '@kit.BasicServicesKit';

// 注入给 H5 的桥对象:方法参数与返回值需为 string/number/boolean 或可序列化结构
class JsBridge {
  getDeviceInfo(): string {
    return JSON.stringify({
      osFullName: deviceInfo.osFullName,
      sdkApiVersion: deviceInfo.sdkApiVersion
    });
  }
  // 原生耗时任务示例:H5 触发后由原生回推结果
  startTask(): void {
    setTimeout(() => {
      try {
        Index.getInstance().controller.runJavaScript(
          `onNativeResult('task done')`);
      } catch (err) {
        const e: BusinessError = err as BusinessError;
        console.error('runJavaScript failed: ' + e.code + ' ' + e.message);
      }
    }, 1000);
  }
}

@Entry
@Component
struct Index {
  controller: webview.WebviewController = new webview.WebviewController();
  private bridge: JsBridge = new JsBridge();
  private static inst: Index | null = null;

  static getInstance(): Index { return Index.inst as Index; }

  aboutToAppear(): void { Index.inst = this; }

  build() {
    Column() {
      Web({ src: $rawfile('index.html'), controller: this.controller })
        .javaScriptAccess(true)
        .onControllerAttached(() => {
          try {
            // 注册同步方法列表;结束后需 deleteJavaScriptRegister 防泄漏
            this.controller.registerJavaScriptProxy(
              this.bridge, 'bridge', ['getDeviceInfo', 'startTask']);
          } catch (err) {
            const e: BusinessError = err as BusinessError;
            console.error('registerJavaScriptProxy failed: ' + e.code);
          }
        })
        .width('100%').height('100%')
      Button('原生调 H5:读取页面标题')
        .onClick(() => {
          this.controller.runJavaScript('document.title')
            .then((r: string) => console.info('page title: ' + r))
            .catch((err: BusinessError) => console.error('err: ' + err.code));
        })
    }
  }
}

运行后点击页面按钮即完成一次"H5 → 原生 → H5"的往返调用;应用底部按钮演示原生主动读取页面状态。

四、应用场景示例

实际业务中常在 H5 加载完成后由原生下发登录态:

Web({ src: 'https://example.com/home', controller: this.controller })
  .javaScriptAccess(true)
  .onPageEnd(() => {
    this.controller.runJavaScript(
      `window.setToken('${this.token}')`)
      .then(() => console.info('token injected'))
      .catch((err: BusinessError) => console.error('err: ' + err.code));
  })
五、技术要点
  1. 注入生效时机:registerJavaScriptProxy 注入的对象在下一次页面加载后才出现在 window 上,务必放在 onControllerAttached 里注册再 loadUrl,或注册后调用 refresh();
  2. 内存泄漏:registerJavaScriptProxy 必须与 deleteJavaScriptRegister('bridge') 配对使用,页面销毁时注销;
  3. 同步与异步方法列表:两个列表不可同时为空;同一方法重复注册时默认按异步调用;异步方法无法取返回值,需要返回值的方法放入同步列表;
  4. 安全白名单:registerJavaScriptProxy 支持 permission 参数(JSON 字符串),可配置 object / method 级别的 scheme+host URL 白名单,防止不可信页面调用桥对象;仅在可信 HTTPS 场景注册;
  5. 数据类型限制:桥方法参数/返回值不支持任意对象,复杂结构用 JSON 字符串序列化;runJavaScript 的返回值是 JSON 字符串,需 JSON.parse 反序列化;
  6. 页面导航丢状态:跨导航后 JS 状态不保留,注入对象不受影响,这也是推荐用 registerJavaScriptProxy 而非 runJavaScript 维持状态的原因。
六、扩展建议

若需要高频、消息式通信(如直播弹幕、传感器数据流),改用 WebMessagePort 建立持久双通道:

let ports = this.controller.createWebMessagePorts();
this.controller.postMessage({ type: 'port', data: 'init' }, 'index.html', [ports[0]]);
ports[1].onMessageEvent((msg: webview.WebMessage) => {
  console.info('from h5: ' + msg.getString());
});

ports[0] 发给页面后,H5 通过 onmessage 监听即可双向收发,避免每次调用都走注入链路。

结语

本文用约 100 行 ArkTS 加一个测试 HTML 实现了 ArkWeb 上完整的 JSBridge:注入桥对象让 H5 直接调用原生能力,runJavaScript 让原生回推结果,并覆盖注入时机、内存泄漏、权限白名单三个最容易踩的坑。这套模式可直接作为混合开发项目的通信基座。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐