4. WebView容器封装与统一接口设计

当Hybrid应用从单页面Demo走向多业务、多端复用时,直接在每个页面里调用WebView、addJavascriptInterface或evaluateJavascript会迅速失控:iOS与Android行为不一致、JSBridge方法散落各处、原生能力升级后旧页面崩溃、H5无法判断当前容器是否支持某个接口。容器封装的目标,就是把“WebView + 原生能力 + 通信协议”收敛为一个稳定、可版本化、可多端对齐的中间层。

容器层的职责划分

一个可维护的容器层通常分为四层,每层只解决一类问题:

  • 宿主层(Host):Activity、ViewController、页面路由、生命周期、权限申请。它决定“什么时候创建容器、什么时候销毁”。
  • 容器层(Container):WebView实例管理、配置项(UA、缓存、DOM存储、混合内容、调试开关)、加载拦截、错误页、进度条、返回键处理。它决定“WebView以什么姿态运行”。
  • 桥接层(Bridge):JSBridge协议解析、方法注册、参数序列化、回调分发、线程切换、超时与异常兜底。它决定“JS与原生如何对话”。
  • 能力层(Capability):相机、定位、支付、文件、剪贴板、设备信息等具体原生能力实现。它决定“能做什么”。

分层的关键收益是:能力层不感知WebView,桥接层不感知具体业务,容器层不感知具体能力。新增一个“扫码”能力时,只需在能力层注册实现,桥接层自动暴露,容器层无需改动。

WebView容器分层架构与JSBridge调用链 宿主层 Host Activity / ViewController · 路由 · 生命周期 · 权限申请 · 容器创建与销毁 容器层 Container WebView配置 · UA · 缓存策略 · 加载拦截 · 错误页 · 进度条 · 返回键 桥接层 Bridge 协议解析 · 方法注册 · 参数序列化 · 回调分发 · 线程切换 · 超时兜底 invoke(method, params) callback(cbId, result) on(event, handler) version() → 协议版本 能力层 Capability 相机 · 定位 · 支付 · 文件 · 剪贴板 · 设备信息 · 扫码 · 分享 H5 / JS 侧 window.bridge.invoke('camera.scan', {…}) Native 侧 CameraCapability.scan(params, callback) JS调用 原生执行 回调

JSBridge接口抽象

JSBridge的本质是一套跨语言RPC协议。设计时不要暴露addJavascriptInterface或WKScriptMessageHandler的原始形态,而应抽象为统一的调用语义:

// H5侧统一调用入口
window.bridge.invoke('camera.scan', {
  quality: 'high',
  format: 'qr'
}, function(result) {
  if (result.code === 0) {
    console.log('扫码结果:', result.data.text);
  } else {
    console.warn('扫码失败:', result.message);
  }
});

// 事件订阅(原生主动通知H5)
window.bridge.on('network.change', function(payload) {
  console.log('网络类型:', payload.type);
});

协议层需要统一三件事:方法命名采用模块.动作格式(如camera.scan、pay.request);参数结构统一为对象,避免位置参数;回调约定统一为{ code, data, message },其中code=0表示成功。这样H5侧只需一套调用代码,iOS与Android各自实现桥接适配即可。

原生能力注入方式

Android侧常用addJavascriptInterface注入一个NativeBridge对象,但需注意API 17以上必须加@JavascriptInterface注解,且注入对象的方法会在WebView的JavaBridge线程执行,不能直接操作UI。更安全的做法是:注入对象只负责接收消息,实际能力调用通过Handler切换到主线程或工作线程执行。

iOS侧WKWebView使用WKScriptMessageHandler接收JS消息,通过evaluateJavaScript回调结果。由于WKWebView不支持同步返回,所有调用必须设计为异步回调模式。两端注入方式不同,但暴露给H5的window.bridge对象应完全一致。

注入时机建议在onPageStarted或didStartProvisionalNavigation时完成,确保H5首屏脚本执行前window.bridge已存在。若H5在DOMContentLoaded之前调用,需在JS侧做就绪检测或队列缓冲。

多端一致性设计

多端一致性不是“代码相同”,而是“行为相同”。需要对齐的维度包括:方法名与参数结构、回调时机与顺序、错误码语义、超时时间、并发调用限制。例如camera.scan在Android上可能返回code=0并携带data.text,iOS也必须返回相同结构,不能一端返回字符串、另一端返回对象。

建议维护一份接口契约表,作为H5、iOS、Android三端共同遵守的“合同”。契约表至少包含:方法名、参数schema、返回值schema、错误码列表、最低支持版本。任何一端修改契约,必须同步更新版本号并通知另外两端。

接口版本管理与兼容策略

版本管理解决的是“旧H5遇到新容器”和“新H5遇到旧容器”两个方向的问题。推荐采用语义化版本:主版本号表示不兼容变更,次版本号表示新增能力,修订号表示修复。桥接层提供bridge.version()返回当前协议版本,H5在启动时读取并决定降级路径。

场景 判断依据 处理策略
新H5调用旧容器不支持的接口 bridge.version()低于接口最低版本 H5降级到Web实现或隐藏入口
旧H5调用已废弃接口 容器侧标记deprecated 容器保留兼容实现,输出警告日志
参数结构变更 主版本号升级 容器同时支持新旧参数解析,按版本分流
能力被移除 主版本号升级且无兼容层 H5必须升级,容器返回明确错误码

兼容策略的核心原则是:新增能力不破坏旧调用,废弃能力先标记后移除。具体做法是容器侧维护一个能力注册表,每个能力声明minVersion和deprecatedVersion。当H5调用时,桥接层先检查版本,若低于minVersion则返回ERR_UNSUPPORTED;若高于deprecatedVersion则返回ERR_DEPRECATED并附带替代方法名。

不要用“容器版本号”直接代替“协议版本号”。容器发版频率远高于协议变更频率,若两者绑定,H5将无法判断某个接口是否真正可用。协议版本应独立维护,仅在接口契约变化时递增。

最后,容器封装与接口设计的验收标准可以归纳为三条:H5侧只依赖window.bridge一个入口;新增原生能力不需要修改H5调用代码;任意一端升级后,另外两端在版本检查通过的前提下行为不变。做到这三点,Hybrid容器才真正具备生产级的稳定性和可演进性。