Administrator
发布于 2026-09-03 / 1,115 阅读
0
0

鸿蒙 WebView Bridge 设计模式:协议规范、安全通信与调试方案

系列文章《鸿蒙开发从入门到专家》第 55 篇 · 高级

第 41 篇讲了 Web 组件怎么用、JS Bridge 怎么注册。第 49 篇讲了怎么把 Web 组件封装成容器框架。这篇聚焦 Bridge 本身:协议怎么设计才安全、跨端通信怎么做错误处理、调试方案怎么搭。

Hybrid 应用里 Bridge 是原生和 H5 之间的唯一通道。通道设计得好,H5 调原生能力像调本地函数一样简单;设计得差,每次新增一个 Bridge 接口都要改三处代码,还得担心注入劫持。

一、Bridge 的通信模型

H5 页面                    ArkWeb Web 组件               原生层
   │                           │                          │
   │  调用 bridge.method()     │                          │
   │ ─────────────────────────>│  JavaProxyProxyObj       │
   │                           │  .onMethodCalled()       │
   │                           │ ─────────────────────────>│
   │                           │                          │ 执行原生逻辑
   │                           │                          │
   │                           │  返回结果/Promise         │
   │                           │ <─────────────────────────│
   │  resolve(result)          │                          │
   │ <─────────────────────────│                          │

核心机制:ArkWeb 提供的 javaScriptProxy 将一个原生对象注入到 H5 的 window 上,H5 调用对象方法时,原生侧的 onMethodCalled 回调被触发。

二、协议设计

2.1 统一消息格式

Bridge 通信采用统一的 JSON 消息格式,而不是每个接口设计不同的参数签名:

// 协议定义
interface BridgeMessage {
  /** 消息 ID,用于异步回调匹配 */
  id: string;
  /** 方法名 */
  method: string;
  /** 参数 */
  params: Record<string, any>;
  /** 调用时间戳 */
  timestamp: number;
}

H5 端封装一个统一的调用接口:

// H5 端 Bridge SDK
window.__HarmonyBridge__ = (function() {
  const callbacks = new Map();
  let seq = 0;

  // 监听原生回传的消息
  window.addEventListener('bridgeCallback', function(event) {
    const data = event.detail;
    const callback = callbacks.get(data.id);
    if (callback) {
      if (data.error) {
        callback.reject(new Error(data.error));
      } else {
        callback.resolve(data.result);
      }
      callbacks.delete(data.id);
    }
  });

  return {
    call: function(method, params) {
      return new Promise(function(resolve, reject) {
        const id = 'msg_' + (++seq) + '_' + Date.now();
        callbacks.set(id, { resolve: resolve, reject: reject });

        // 调用注入的原生对象
        window.nativeBridge.postMessage(JSON.stringify({
          id: id,
          method: method,
          params: params || {},
          timestamp: Date.now()
        }));

        // 超时处理
        setTimeout(function() {
          if (callbacks.has(id)) {
            callbacks.delete(id);
            reject(new Error('Bridge call timeout: ' + method));
          }
        }, 10000);
      });
    }
  };
})();

2.2 原生端处理

// bridge/BridgeHandler.ets
import { webview } from '@kit.ArkWeb';

interface BridgeMessage {
  id: string;
  method: string;
  params: Record<string, any>;
  timestamp: number;
}

interface BridgeHandler {
  method: string;
  handler: (params: Record<string, any>) => Promise<any>;
}

export class BridgeManager {
  private controller: webview.WebviewController;
  private handlers: Map<string, BridgeHandler['handler']> = new Map();

  // 注册 Bridge 方法
  register(method: string, handler: (params: Record<string, any>) => Promise<any>): void {
    this.handlers.set(method, handler);
  }

  // 注入到 H5
  inject(webView: WebController): void {
    webView.javaScriptProxy({
      object: this,
      name: 'nativeBridge',
      methodList: ['postMessage'],
      asyncMethodList: [],
      permission: '__BridgeInternal__'
    });
  }

  // H5 调用入口
  postMessage(rawMessage: string): void {
    let msg: BridgeMessage;
    try {
      msg = JSON.parse(rawMessage) as BridgeMessage;
    } catch (e) {
      console.error('Bridge: invalid message format');
      return;
    }

    // 安全检查
    if (!this.handlers.has(msg.method)) {
      this.sendCallback(msg.id, null, `Unknown method: ${msg.method}`);
      return;
    }

    const handler = this.handlers.get(msg.method)!;
    handler(msg.params)
      .then(result => {
        this.sendCallback(msg.id, result, null);
      })
      .catch(error => {
        this.sendCallback(msg.id, null, error.message || 'Unknown error');
      });
  }

  // 回调 H5
  private sendCallback(id: string, result: any, error: string | null): void {
    const callbackData = JSON.stringify({ id, result, error });
    // 通过 evaluateJS 回调 H5
    this.controller.runJavaScript(
      `window.dispatchEvent(new CustomEvent('bridgeCallback', { detail: ${callbackData} }));`
    );
  }
}

三、安全设计

Bridge 是 H5 调原生能力的入口,安全管控不到位等于给恶意页面开后门。

3.1 URL 白名单

只有可信域名的页面才能使用 Bridge:

export class SecurityGuard {
  private whitelist: string[] = [
    'https://app.example.com',
    'https://*.example.com',
    'file:///android_asset/'
  ];

  isUrlAllowed(url: string): boolean {
    return this.whitelist.some(pattern => {
      if (pattern.includes('*')) {
        // 通配符匹配
        const regex = new RegExp(
          '^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'
        );
        return regex.test(url);
      }
      return url.startsWith(pattern);
    });
  }
}

在 Bridge 调用入口做检查:

postMessage(rawMessage: string): void {
  const currentUrl = this.controller.getUrl();

  if (!this.securityGuard.isUrlAllowed(currentUrl)) {
    console.error(`Bridge: blocked call from ${currentUrl}`);
    return;
  }
  // ... 后续处理
}

3.2 方法权限分级

不是所有 Bridge 方法对所有 H5 页面开放。按敏感度分级:

级别

含义

允许的域名

public

公开只读(如获取设备型号)

白名单内所有

protected

业务操作(如打开支付页)

业务域名

private

敏感操作(如读取通讯录)

仅本应用包内 H5

enum BridgePermission {
  PUBLIC = 'public',
  PROTECTED = 'protected',
  PRIVATE = 'private'
}

interface BridgeHandlerConfig {
  method: string;
  handler: (params: Record<string, any>) => Promise<any>;
  permission: BridgePermission;
}

// 注册时声明权限
bridgeManager.register({
  method: 'getDeviceInfo',
  handler: getDeviceInfo,
  permission: BridgePermission.PUBLIC
});

bridgeManager.register({
  method: 'openPayment',
  handler: openPayment,
  permission: BridgePermission.PROTECTED
});

bridgeManager.register({
  method: 'readContacts',
  handler: readContacts,
  permission: BridgePermission.PRIVATE
});

调用时根据当前页面域名和方法的权限级别做匹配:

private checkPermission(method: string, url: string): boolean {
  const config = this.handlers.get(method);
  if (!config) return false;

  switch (config.permission) {
    case BridgePermission.PUBLIC:
      return this.securityGuard.isUrlAllowed(url);
    case BridgePermission.PROTECTED:
      return url.includes('app.example.com');
    case BridgePermission.PRIVATE:
      return url.startsWith('file:///') || url.startsWith('resource://');
    default:
      return false;
  }
}

四、错误处理

Bridge 调用的错误来源有三类:协议错误(消息格式不对)、方法错误(方法不存在或参数校验失败)、执行错误(原生逻辑执行失败)。

统一错误码体系:

enum BridgeErrorCode {
  // 协议级
  INVALID_MESSAGE = 1001,
  UNKNOWN_METHOD = 1002,
  INVALID_PARAMS = 1003,

  // 权限级
  URL_BLOCKED = 2001,
  PERMISSION_DENIED = 2002,

  // 执行级
  EXECUTION_FAILED = 3001,
  TIMEOUT = 3002
}

class BridgeError extends Error {
  constructor(public code: BridgeErrorCode, message: string) {
    super(message);
  }

  toJSON(): string {
    return JSON.stringify({
      code: this.code,
      message: this.message
    });
  }
}

H5 端可以根据错误码做不同处理:

window.__HarmonyBridge__.call('openPayment', { orderId: '123' })
  .then(function(result) {
    // 成功
  })
  .catch(function(error) {
    if (error.message.includes('2002')) {
      // 权限不足,提示用户
      alert('当前页面无权调用支付');
    } else if (error.message.includes('3002')) {
      // 超时,重试
      retry();
    }
  });

五、调试方案

Bridge 的调试难点在于 H5 和原生分属两个调试环境。H5 用 Chrome DevTools,原生用 DevEco Profiler。

5.1 日志桥

在 Bridge 调用入口和出口打日志,统一格式:

private logBridgeCall(msg: BridgeMessage, url: string): void {
  console.info(
    `[Bridge] CALL | id=${msg.id} method=${msg.method} url=${url} params=${JSON.stringify(msg.params)}`
  );
}

private logBridgeResult(id: string, result: any, error: string | null): void {
  if (error) {
    console.error(
      `[Bridge] ERROR | id=${id} error=${error}`
    );
  } else {
    console.info(
      `[Bridge] RESULT | id=${id} result=${JSON.stringify(result)}`
    );
  }
}

通过 id 可以在日志中串联一次完整的调用链。

5.2 开发模式注入 Mock

本地开发时,H5 可能跑在本地 dev server 上,原生侧的功能还没实现。可以在开发模式下注入 Mock:

// 开发模式注入 Mock Bridge
if (BuildConfig.DEBUG) {
  this.controller.runJavaScript(`
    window.nativeBridge = {
      postMessage: function(msg) {
        var parsed = JSON.parse(msg);
        setTimeout(function() {
          // Mock 返回
          window.dispatchEvent(new CustomEvent('bridgeCallback', {
            detail: JSON.stringify({
              id: parsed.id,
              result: { mock: true, method: parsed.method },
              error: null
            })
          }));
        }, 100);
      }
    };
  `);
  // 不注入真实的 javaScriptProxy
  return;
}

H5 端完全无感知,可以独立开发和联调。

六、常用 Bridge 接口清单

一个完整的 Hybrid 应用通常需要的 Bridge 接口:

方法

权限

功能

getDeviceInfo

public

设备型号、系统版本

getAppInfo

public

应用版本、渠道

openPage

protected

打开原生页面

closeWebView

protected

关闭当前 Web 容器

openPayment

protected

拉起支付

getUserToken

protected

获取登录 token

takePhoto

private

调用相机

chooseImage

private

选择相册图片

getLocation

protected

获取定位

shareTo

protected

分享到社交平台

小结

Bridge 设计的三个核心:协议统一(JSON 消息格式)、安全分级(URL 白名单 + 方法权限)、调试可观测(日志桥 + Mock 模式)。不要让每个 Bridge 接口自己定义参数签名和回调方式,统一走消息格式 + id 匹配的方式,扩展时只加方法不改编译协议。

下篇讲 IPC 跨进程通信。DataShareExtension 和跨应用数据流转怎么实现。

参考阅读:

  • 第 41 篇:鸿蒙 ArkWeb 实战(Web 组件嵌入)

  • 第 49 篇:鸿蒙 H5 容器化(容器框架设计)

  • 官方文档:javaScriptProxy、WebviewController


评论