H5 运行在 App WebView 中,需要调用登录、旋转屏幕、分享和设备能力。最初的 JSBridge 很直接:前端调用约定好的全局函数,原生端再回调另一个函数。功能少时足够,端版本增多后,问题开始集中出现。

旧版本没有新方法,Android 与 iOS 返回结构不同,页面销毁后回调仍然到达。每个业务模块都加一套平台判断,通信层逐渐失去边界。

把调用建模为消息

我们把一次调用定义为包含方法、参数、调用标识和协议版本的消息,响应则包含相同标识、结果与标准错误。调用标识解决并发回调对应问题,超时让调用不会永久悬挂,统一错误让业务能决定重试或降级。

前端不直接访问平台对象,而是依赖一个 Promise 接口。原生端能力通过启动时的协商表暴露,页面可以先判断是否支持,而不是调用后再猜测。

协议消息可以保持很小,但字段含义必须稳定:

type BridgeRequest = {
  id: string;
  method: string;
  version: 2;
  params: Record<string, unknown>;
};
 
type BridgeResponse<T> =
  | { id: string; ok: true; result: T }
  | { id: string; ok: false; error: { code: string; message: string } };
 
type BridgeCapabilities = Record<string, { version: number }>;

调用层为每个 id 保存 Promise 的 resolve/reject 和超时句柄。原生响应回来后按 id 精确配对,完成后立即删除。若同一响应重复到达,桥接层记录异常但不再次触发业务回调。

const pending = new Map<string, PendingCall>();
 
function invoke<T>(method: string, params: object, timeoutMs = 5000): Promise<T> {
  if (!capabilities[method]) return Promise.reject(new UnsupportedMethod(method));
  const id = crypto.randomUUID();
  return new Promise<T>((resolve, reject) => {
    const timer = window.setTimeout(() => {
      pending.delete(id);
      reject(new BridgeTimeout(method));
    }, timeoutMs);
    pending.set(id, { resolve, reject, timer, method });
    nativeTransport.postMessage({ id, method, version: 2, params });
  });
}

兼容性是协议的一部分

App 发布后无法要求所有用户立刻升级。新增能力必须考虑旧端:参数只做向后兼容扩展,破坏性变化使用新方法名或协议版本,无法支持时返回明确错误。

日志也应保留方法、耗时与错误码,但不记录敏感参数。跨端问题往往只能在特定设备复现,没有通信层证据,排查成本会非常高。

错误码要让调用方能行动,而不是把原生异常字符串透传回来:

错误 是否重试 页面行为
UNSUPPORTED 隐藏入口或展示升级说明
USER_CANCELLED 保持当前状态,不弹系统错误
PERMISSION_DENIED 用户授权后 引导到权限设置
TIMEOUT 视方法而定 对只读调用可重试,写操作先查询结果
NATIVE_FAILURE 展示可追踪错误编号

生命周期需要显式管理

WebView 页面可能在原生调用完成前离开。桥接层要在页面销毁时清理未完成回调,拒绝对应 Promise,并阻止晚到结果修改新页面。事件订阅同样需要返回取消函数,避免页面多次进入后重复监听。

对于横屏、网络和登录状态这类持续事件,约定初始快照与后续变更的顺序。只有事件而没有初值,页面启动时会处于未知状态;只有查询而没有订阅,状态变化又无法及时反映。协议必须覆盖完整生命周期。

JSBridge 看似只是几段胶水代码,实际连接了两个独立发布的系统。只要两端不能同时更新,它就应该像公开 API 一样被设计和维护。

H5、Bridge、Native 与系统能力之间带版本和超时的调用时序

图:可靠 Bridge 需要 requestId、来源校验和结构化失败。

Bridge 兼容矩阵是协议的一部分

每个能力记录首次支持的 iOS/Android 版本、参数版本、超时、重复回调和 H5 降级。页面启动时读取 capability 列表,不用 UA 猜测。

capability: file.pick
schema: v2
iOS: >= 6.4
Android: >= 6.6
timeout: 30s
fallback: HTML file input

线上错误按 capability + nativeVersion 聚合,才能区分协议不兼容和业务失败。