大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

混合应用里有一类很典型的需求:页面主体由 H5 实现,但登录态、设备能力、系统页面跳转等能力掌握在应用侧。H5 需要调用 ArkTS,ArkTS 处理完成后又要把结果送回网页。

如果只是单向通知,这件事并不复杂。真正容易把代码写乱的是“H5 发起请求 → ArkTS 接收参数 → 执行业务 → ArkTS 回调 H5 → H5 恢复对应 Promise”这一整条链路。

这次用一个本地 H5 最小示例,把 ArkWeb 中这条双向通信链路完整串起来,并进一步封装成一个统一 Bridge。

一、为什么混合应用需要 JS Bridge

先看一个具体场景。

假设应用中有一个活动页由 H5 开发。页面需要两个能力:

  1. H5 传入两个数字,让 ArkTS 完成计算并立即返回;
  2. H5 发起一个异步请求,ArkTS 处理完成后主动回调 H5。

这两个需求对应两条方向相反的通道:

H5 -> ArkTS
JavaScriptProxy / registerJavaScriptProxy

ArkTS -> H5
WebviewController.runJavaScript()

华为 ArkWeb 官方开发指南把“应用侧调用前端页面函数”“前端页面调用应用侧函数”和“建立应用侧与前端页面数据通道”分别作为 Web 与 JavaScript 交互能力进行说明。对于持续的消息型通信,官方还提供 createWebMessagePorts() 创建消息端口。

本文不做消息端口方案,而是聚焦更接近传统 JS Bridge 的调用模型:

H5
 │
 │ window.NativeBridge.invoke(...)
 ▼
ArkTS
 │
 │ runJavaScript(...)
 ▼
H5 callback

这样做的好处是业务层可以继续使用“方法名 + 参数 + Promise”的调用习惯,而不用让每个 H5 页面自己拼 ArkWeb API。

二、先确认版本和能力边界

本文以 HarmonyOS 7、API 26、Stage 模型应用作为目标开发背景。

华为官方已经明确,HarmonyOS 7.0 对应 API 26.0.0;从 26.0.0 开始,HarmonyOS 开发套件的 API 版本号改用 X.Y.Z 语义化版本格式。官方同时建议面向 HarmonyOS 7 的应用使用 26.0.0 开发套件进行升级适配。

这里使用的核心模块是:

import { webview } from '@kit.ArkWeb';

核心对象是:

webview.WebviewController

本文涉及的主要能力包括:

能力用途
Web在 ArkUI 页面中承载 H5
$rawfile()引用应用包内的本地网页资源
javaScriptProxy()Web 初始化时把 ArkTS 对象注册到网页环境
runJavaScript()应用侧执行当前网页上下文中的 JavaScript
deleteJavaScriptRegister()删除已经注册的 JavaScriptProxy 对象

runJavaScript() 是异步执行接口,执行结果通过 Promise 返回;官方 FAQ 中也明确说明,它在当前显示页面上下文执行 JavaScript,并要求在 UI 线程使用。

还有一个边界必须单独说明:**本文讨论的是 HarmonyOS 应用中的 ArkWeb Web 组件,不是元服务的 AtomicServiceEnhancedWeb。**截至 2026 年 9 月的官方 FAQ,AtomicServiceEnhancedWeb 暂不支持 runJavaScript() 和 registerJavaScriptProxy(),不能把本文代码直接套到该组件上。

这个区别很容易被忽略。

三、先搭一个最小实践

工程中准备一个本地网页:

entry
└── src
    └── main
        ├── ets
        │   └── pages
        │       └── Index.ets
        └── resources
            └── rawfile
                └── bridge
                    └── index.html

本文只加载应用包中的 $rawfile() 本地页面,把网络请求、登录、路由等业务全部拿掉。

最终希望实现两个调用:

HarmonyBridge.call('sum', {
  a: 10,
  b: 20
});

立即得到:

30

再调用:

HarmonyBridge.call('delayEcho', {
  message: 'Hello ArkTS'
});

ArkTS 先接收请求,异步处理结束后再主动执行 H5 中的回调函数,让这个调用最终仍然表现为一个 Promise。

四、先实现 H5 调 ArkTS

1. 定义统一请求结构

如果每增加一个能力就往 window 上暴露一个方法:

getUser()
openPage()
scan()
pay()
getLocation()
...

Bridge 很快就会失去边界。

更容易维护的办法是只暴露一个入口:

NativeBridge.invoke(requestJson)

请求统一成:

{
  "id": "req_001",
  "method": "sum",
  "params": "{\"a\":10,\"b\":20}"
}

id 用于异步结果匹配,method 表示要调用的能力,params 保存业务参数。

2. ArkTS 侧 Bridge

下面代码按照 ArkWeb 官方 JavaScriptProxy 和 runJavaScript() 的接口形式组织为最小示例。由于这里没有实际执行 HarmonyOS 工程编译,发布前仍应使用目标 API 26 SDK 做一次工程级校验。

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

interface BridgeRequest {
  id: string;
  method: string;
  params: string;
}

interface BridgeResponse {
  id: string;
  ok: boolean;
  pending: boolean;
  data: string;
  error: string;
}

interface SumParams {
  a: number;
  b: number;
}

interface EchoParams {
  message: string;
}

const webController: webview.WebviewController =
  new webview.WebviewController();

function createResponse(
  id: string,
  ok: boolean,
  pending: boolean,
  data: string,
  error: string
): BridgeResponse {
  return {
    id,
    ok,
    pending,
    data,
    error
  };
}

function callbackToH5(response: BridgeResponse): void {
  const responseJson = JSON.stringify(response);

  // 再 stringify 一次,把 JSON 文本安全地变成 JS 字符串字面量。
  const script =
    `window.HarmonyBridge.__resolve(${JSON.stringify(responseJson)})`;

  webController.runJavaScript(script)
    .catch((error: BusinessError) => {
      console.error(
        `runJavaScript failed, code=${error.code}, message=${error.message}`
      );
    });
}

class NativeBridge {
  invoke(requestJson: string): string {
    try {
      const request = JSON.parse(requestJson) as BridgeRequest;

      switch (request.method) {
        case 'sum': {
          const params = JSON.parse(request.params) as SumParams;
          const result = params.a + params.b;

          return JSON.stringify(
            createResponse(
              request.id,
              true,
              false,
              result.toString(),
              ''
            )
          );
        }

        case 'delayEcho': {
          const params = JSON.parse(request.params) as EchoParams;

          setTimeout(() => {
            callbackToH5(
              createResponse(
                request.id,
                true,
                false,
                `ArkTS received: ${params.message}`,
                ''
              )
            );
          }, 500);

          return JSON.stringify(
            createResponse(
              request.id,
              true,
              true,
              '',
              ''
            )
          );
        }

        default:
          return JSON.stringify(
            createResponse(
              request.id,
              false,
              false,
              '',
              `Unknown method: ${request.method}`
            )
          );
      }
    } catch (error) {
      return JSON.stringify(
        createResponse(
          '',
          false,
          false,
          '',
          `Invalid bridge request: ${String(error)}`
        )
      );
    }
  }
}

const nativeBridge = new NativeBridge();

这里真正需要关注的不是 switch,而是协议。

同步调用直接返回完整 BridgeResponse;异步调用先返回:

{
  "pending": true
}

等业务结束后,再通过 runJavaScript() 把最终结果推给网页。

这样同步和异步能力可以共用一个入口。

五、把 Bridge 注入 Web 页面

页面部分保持很小:

@Entry
@Component
struct Index {
  aboutToDisappear(): void {
    try {
      webController.deleteJavaScriptRegister('NativeBridge');
    } catch (error) {
      const err = error as BusinessError;
      console.error(
        `deleteJavaScriptRegister failed: ${err.code}, ${err.message}`
      );
    }
  }

  build() {
    Column() {
      Web({
        src: $rawfile('bridge/index.html'),
        controller: webController
      })
        .width('100%')
        .height('100%')
        .javaScriptAccess(true)
        .javaScriptProxy({
          object: nativeBridge,
          name: 'NativeBridge',
          methodList: ['invoke'],
          asyncMethodList: [],
          controller: webController,
          permission:
            '{"javascriptProxyPermission":{' +
            '"urlPermissionList":[' +
            '{"scheme":"resource",' +
            '"host":"rawfile",' +
            '"port":"",' +
            '"path":""}' +
            ']}}'
        });
    }
    .width('100%')
    .height('100%');
  }
}

这里没有把几十个 Native 方法直接暴露出去,只注册:

NativeBridge.invoke

同时给 JavaScriptProxy 配置了 URL 权限范围,只允许:

resource://rawfile

这一类本地资源来源调用 Bridge。

官方 JavaScriptProxy 示例同样提供了 javascriptProxyPermission、urlPermissionList 以及 scheme、host、port、path 等粒度的限制方式;在官方 H5 适配指导中,也可以看到 registerJavaScriptProxy() 配合 URL 权限范围的用法。

六、H5 侧把调用包装成 Promise

接下来处理网页。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1.0">
  <title>ArkWeb Bridge Demo</title>
</head>

<body>
  <button onclick="testSum()">同步调用</button>
  <button onclick="testAsync()">异步调用</button>

  <pre id="result"></pre>

  <script>
    const pendingCalls = new Map();
    let requestSeed = 0;

    window.HarmonyBridge = {
      call(method, params = {}) {
        return new Promise((resolve, reject) => {
          if (!window.NativeBridge ||
              typeof window.NativeBridge.invoke !== 'function') {
            reject(new Error('NativeBridge is not available'));
            return;
          }

          const id = `req_${Date.now()}_${++requestSeed}`;

          pendingCalls.set(id, {
            resolve,
            reject
          });

          const request = {
            id,
            method,
            params: JSON.stringify(params)
          };

          try {
            const raw =
              window.NativeBridge.invoke(JSON.stringify(request));

            const response = JSON.parse(raw);

            // pending=true 表示 ArkTS 稍后主动回调。
            if (response.pending) {
              return;
            }

            pendingCalls.delete(id);

            if (response.ok) {
              resolve(response.data);
            } else {
              reject(new Error(response.error));
            }
          } catch (error) {
            pendingCalls.delete(id);
            reject(error);
          }
        });
      },

      __resolve(responseJson) {
        const response = JSON.parse(responseJson);
        const pending = pendingCalls.get(response.id);

        if (!pending) {
          return;
        }

        pendingCalls.delete(response.id);

        if (response.ok) {
          pending.resolve(response.data);
        } else {
          pending.reject(new Error(response.error));
        }
      }
    };

    async function testSum() {
      try {
        const result =
          await HarmonyBridge.call('sum', {
            a: 10,
            b: 20
          });

        document.getElementById('result').textContent =
          `sum result: ${result}`;
      } catch (error) {
        document.getElementById('result').textContent =
          String(error);
      }
    }

    async function testAsync() {
      try {
        const result =
          await HarmonyBridge.call('delayEcho', {
            message: 'Hello ArkTS'
          });

        document.getElementById('result').textContent =
          result;
      } catch (error) {
        document.getElementById('result').textContent =
          String(error);
      }
    }
  </script>
</body>
</html>

到这里,H5 已经不需要知道 runJavaScript()、WebviewController 或 ArkTS 类是什么。

业务页面只认识:

await HarmonyBridge.call(method, params);

这正是统一 Bridge 最有价值的地方:平台通信细节被压到桥接层,业务代码只处理方法、参数和结果。

七、ArkTS 调 H5,为什么不能只靠字符串拼接

ArkTS 回调网页的关键代码是:

webController.runJavaScript(script);

官方说明中,runJavaScript() 会在当前页面上下文异步执行 JavaScript;如果需要获得更丰富的 JavaScript 返回类型,ArkWeb 还提供 runJavaScriptExt() 和对应的 JsMessageExt。当前官方 API 文档中,JsMessageExt 可以区分字符串、数值、布尔值、ArrayBuffer、数组等结果类型。

不过 Bridge 回调还有另一个问题:数据不能直接裸拼到 JavaScript 源码中。

例如不要这样写:

const script =
  `window.onResult('${message}')`;

如果 message 本身包含引号、换行甚至 JavaScript 片段,最终生成的脚本可能改变原来的语义。

示例采用:

JSON.stringify(responseJson)

先把参数编码成合法的 JavaScript 字符串字面量,再拼入要执行的函数调用。

实际项目里,这一步很容易因为“正常中文字符串都能工作”而被忽略。

八、返回结果和异步调用怎么设计

同步调用比较直接:

H5 invoke()
   ↓
ArkTS 执行
   ↓
return JSON
   ↓
H5 Promise resolve

异步调用不能假设 ArkTS 的业务会立即结束。

所以这里增加了 requestId:

req_172...

流程变成:

H5 创建 requestId
        ↓
pendingCalls 保存 Promise
        ↓
NativeBridge.invoke()
        ↓
ArkTS 返回 pending=true
        ↓
ArkTS 异步任务执行
        ↓
runJavaScript()
        ↓
HarmonyBridge.__resolve()
        ↓
按 requestId 找到 Promise
        ↓
resolve / reject

这套结构还解决了并发问题。

如果同时发出三个请求:

req_1
req_2
req_3

即使返回顺序变成:

req_3
req_1
req_2

H5 也能根据 ID 找回对应的 Promise,而不是依赖“谁先请求谁先返回”。

如果业务更适合持续、高频的数据交换,而不是 RPC 式的一问一答,则可以评估 ArkWeb 官方提供的 WebMessagePort。官方文档明确给出了 createWebMessagePorts()、postMessage()、postMessageEvent() 和 onMessageEvent() 建立双向数据通道的方案;WebMessage 支持 string 和 ArrayBuffer,对象数据可以先通过 JSON 序列化为 string。

所以不要把所有 Web 与 Native 通信都强行塞进一种 Bridge。

九、不可信网页为什么不能随意暴露 Native 方法

JS Bridge 最需要警惕的地方其实不是参数类型,而是能力边界。

一旦把:

NativeBridge

注入网页,这个对象就不再只是 ArkTS 内部代码。

网页脚本可以调用其中被允许的方法。

如果同一个 Web 组件既加载自己的本地页面,又可能跳转到外部网页,却给所有来源暴露诸如:

getToken
readUserData
openNativePage
deleteFile
pay

这样的能力,Bridge 就会从通信接口变成攻击面。

因此至少要把三件事做好:

第一,只暴露必要方法。

本文只注册:

methodList: ['invoke']

业务能力再在 invoke() 内部做白名单分发,而不是把整个对象的方法都开放给网页。

第二,限制允许调用 Bridge 的页面来源。

本文通过:

{
  "scheme": "resource",
  "host": "rawfile"
}

限制到应用内本地资源来源。

如果以后切换到 HTTPS 在线页面,应根据实际业务域名配置对应的 JavaScriptProxy URL 权限,而不是为了“省事”把来源范围无限放大。

第三,Bridge 内部仍然要校验 method 和参数。

网页传来:

{
  "method": "anything"
}

不能直接通过反射或动态属性访问执行任意 Native 方法。

本文采用明确的:

switch (request.method)

未知方法直接返回错误:

Unknown method

Bridge 应该被当作应用对 Web 开放的一组 API,而不是 ArkTS 世界的一扇后门。

十、几个容易理解错的地方

1. runJavaScript() 不等于“调用固定 H5 API”

它本质上执行的是 JavaScript 脚本。

所以:

runJavaScript('htmlTest()')

可以调用网页函数,也可以执行其他合法 JavaScript。

官方 FAQ 也采用在 onPageEnd 后通过 runJavaScript() 操作页面 DOM 的方式说明这一能力。

2. Bridge 可用时机和页面加载时机不是一回事

WebviewController 必须和 Web 组件建立关联后,实例方法才能正常工作。

而网页中的函数又必须已经进入当前页面上下文。

因此如果 ArkTS 一创建页面就立即:

runJavaScript('window.xxx()')

但 H5 还没有定义 window.xxx,自然无法得到预期结果。

需要应用启动后主动向 H5 推送初始化数据时,可以结合 Web 页面生命周期设计初始化时机,而不是用固定延时“猜”页面什么时候准备好。

3. 对象参数最好建立自己的协议

不要让 Bridge 一会儿传对象、一会儿传数组、一会儿传多个位置参数。

统一成 JSON 协议后:

id
method
params

日志、错误处理、版本升级都会简单很多。

4. Bridge 不再使用时要解除注册

JavaScriptProxy 不应该只注册不释放。

页面或 Bridge 生命周期结束后,应结合页面结构调用 deleteJavaScriptRegister() 解除已经注册的对象。

十一、实际项目中怎么排查

如果 H5 调 ArkTS 没反应,可以按下面顺序查:

  1. 先确认组件。 当前页面到底使用的是应用 ArkWeb Web,还是元服务 AtomicServiceEnhancedWeb。后者目前不能直接套用本文的 runJavaScript() / registerJavaScriptProxy() 方案。
  2. 再确认版本。 HarmonyOS 7 对应 API 26.0.0,项目 SDK、设备系统版本和实际调用接口要对应。
  3. 检查 JavaScript 是否开启以及 Bridge 是否完成注册。 H5 可以先打印 window.NativeBridge,确认对象是否存在。
  4. 检查来源权限。 如果配置了 javascriptProxyPermission,当前网页的 scheme、host、port、path 必须落在允许范围内。
  5. 检查方法名。 methodList 中没有暴露的方法不能按已注册 Bridge 方法使用。
  6. 检查参数协议。 JSON 是否能正常解析,字段类型是否符合约定。
  7. 检查回调时机。 ArkTS 调用 H5 函数时,该函数是否已经在当前 document 中定义。
  8. 最后看 requestId。 异步回调到达 H5 后,pendingCalls 中是否仍存在对应 ID。

这套顺序比一上来怀疑 ArkWeb 内核更容易缩小问题范围。

十二、什么时候改用 WebMessagePort

JavaScriptProxy + runJavaScript() 很适合“调用一个能力,等待一个结果”的 RPC 风格。

例如:

getUserInfo
openNativePage
chooseFile
queryConfig
startNativeTask

如果需求变成持续交换数据:

Native 连续推送状态
H5 高频发送消息
双方长期保持通信通道

就应该评估 WebMessagePort。

官方的数据通道方案是由应用侧创建两个消息端口,把其中一个通过 postMessage() 交给前端页面,双方随后分别持有端口进行通信,并在不再使用或 Webview 销毁前关闭端口。

也就是说,Bridge 的设计重点不是“找到唯一正确的 API”,而是先判断自己的通信模型。

开发经验总结

这套最小实践真正值得留下来的不是 sum() 或 delayEcho(),而是五个设计点。

一是把双向通信拆清楚。 H5 调 ArkTS 由 JavaScriptProxy 建立入口;ArkTS 主动回调 H5,可以使用 runJavaScript()。

二是不要让业务层直接依赖 ArkWeb。 用统一的:

HarmonyBridge.call(method, params)

把平台差异收口。

三是异步调用一定要有 requestId。 只要存在并发请求,就不能依赖调用顺序匹配返回结果。

四是 Bridge 本身就是安全边界。 暴露的方法越少越好,来源范围越明确越好,Native 侧仍然需要验证 method 和参数。

五是根据通信模型选机制。 一次请求一次返回适合 JS Bridge;持续双向消息可以继续评估官方 WebMessagePort 数据通道。

HarmonyOS 7 已正式进入 API 26 开发阶段,版本升级时除了关注新增 API,也应该重新检查这类跨运行环境接口的权限边界和生命周期。官方升级指南明确建议应用结合 API 变化进行适配评估。

如果项目里已经有一套 Android/iOS WebView Bridge,也可以进一步思考一个问题:**业务层协议能不能保持不变,只把 HarmonyOS ArkWeb 的 JavaScriptProxy 和 runJavaScript() 封装成新的平台适配层?**做到这一点之后,混合页面真正需要维护的就不再是三套 Bridge,而是一套协议、多个平台实现。

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

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

更多推荐