🔗 Hybrid 开发与 JSBridge 通信

Hybrid App 是 H5 页面嵌入原生 App WebView 的开发模式。核心在于 JSBridge——JS 与原生代码互相调用的桥梁。

Hybrid 架构示意

┌──────────────────────────────────────────┐
│              原生 App 容器                │
│  ┌────────────────────────────────────┐  │
│  │          WebView (H5 页面)         │  │
│  │  ┌──────────────────────────────┐  │  │
│  │  │  HTML + CSS + JS 业务逻辑    │  │  │
│  │  └──────────┬───────────────────┘  │  │
│  │             │ JSBridge              │  │
│  └─────────────┼──────────────────────┘  │
│                │                         │
│  ┌─────────────▼──────────────────────┐  │
│  │     原生能力 (相机/定位/支付/推送)   │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘

JSBridge 通信原理

方案原理适用平台
URL Scheme 拦截 JS 发起自定义协议请求(如 myapp://),原生拦截解析 Android / iOS 通用
注入 JS 对象 Android 通过 addJavascriptInterface,iOS 通过 WKScriptMessageHandler 注入原生对象到 JS 上下文 Android / iOS 各不同
evaluateJavaScript 原生直接执行 JS 代码(常用于原生调用 JS) Android / iOS 通用
Prompt 拦截 WebView 拦截 window.prompt() 传递消息 Android(早期方案)

JSBridge 调用封装

// ===== 通用 JSBridge 调用封装 =====
const JSBridge = {
  // 判断是否在 App 环境中
  isApp() {
    return /MyApp/i.test(navigator.userAgent);
  },

  // 调用原生方法
  call(method, params = {}) {
    const data = JSON.stringify({ method, params });

    // Android: 注入的对象
    if (window.NativeBridge) {
      window.NativeBridge.invoke(method, JSON.stringify(params));
      return;
    }

    // iOS: WKWebView messageHandlers
    if (window.webkit?.messageHandlers?.NativeBridge) {
      window.webkit.messageHandlers.NativeBridge.postMessage({ method, params });
      return;
    }

    // 降级方案: URL Scheme
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = `myapp://bridge?data=${encodeURIComponent(data)}`;
    document.body.appendChild(iframe);
    setTimeout(() => iframe.remove(), 100);
  },

  // 注册回调(原生调用 JS)
  on(event, callback) {
    window[`__bridge_callback_${event}`] = callback;
  }
};

// 使用示例
JSBridge.call('getLocation');
JSBridge.call('share', { title: '分享标题', url: location.href });
JSBridge.on('locationResult', (data) => {
  console.log('定位结果:', data.latitude, data.longitude);
});
⚠️ JSBridge 常见坑
  • 回调丢失:页面跳转或 WebView 销毁时回调可能丢失,需要超时兜底机制
  • 注入时机:JSBridge 对象可能在 DOMContentLoaded 之后才注入,需要轮询等待
  • 参数序列化:Android 部分版本不支持 JSON 对象传递,需要 JSON.stringify
  • 线程问题:iOS WKWebView 回调在非主线程,更新 UI 需 dispatch 到主线程
  • URL Scheme 长度限制:部分设备限制 URL 约 2KB,大数据用注入方式

微信 JSSDK 使用

微信内置浏览器提供了 JSSDK,用于调用微信原生能力(分享、支付、图像、音频等)。

<!-- ===== 微信 JSSDK 集成 ===== -->
<!-- 1. 引入 JS 文件 -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>

<script>
// 2. 通过后端接口获取签名配置
async function initWxConfig() {
  const resp = await fetch('/api/wx-signature?url=' + encodeURIComponent(location.href));
  const config = await resp.json();

  wx.config({
    debug: false,          // 开发时可开启 debug 模式
    appId: config.appId,
    timestamp: config.timestamp,
    nonceStr: config.nonceStr,
    signature: config.signature,
    jsApiList: [           // ⭐ 需要使用的 JS 接口列表
      'updateAppMessageShareData',  // 自定义分享给朋友
      'updateTimelineShareData',    // 自定义分享到朋友圈
      'chooseImage',                // 选择图片
      'previewImage',               // 预览图片
      'getLocation',                // 获取地理位置
      'scanQRCode',                 // 扫码
    ]
  });

  wx.ready(() => {
    // 配置成功后,调用分享接口
    wx.updateAppMessageShareData({
      title: '分享标题',
      desc: '分享描述',
      link: location.href,
      imgUrl: 'https://example.com/share-icon.jpg',
    });
  });

  wx.error((res) => {
    console.error('微信 JSSDK 配置失败:', res);
  });
}

initWxConfig();

// 3. 常用微信 JSSDK API 调用示例
// 选择图片
wx.chooseImage({
  count: 1,
  sizeType: ['compressed'],
  sourceType: ['album', 'camera'],
  success(res) {
    const localIds = res.localIds;
    console.log('选中图片:', localIds);
  }
});

// 获取地理位置
wx.getLocation({
  type: 'wgs84',
  success(res) {
    console.log(`纬度:${res.latitude}, 经度:${res.longitude}`);
  }
});
</script>
💡 微信 JSSDK 注意事项
  • 签名 URL:签名用的 URL 必须是当前页面完整 URL(不含 hash),iOS 用第一次进入的 URL
  • 域名绑定:JS 接口安全域名需在公众号后台配置,不支持 IP 和端口
  • SPA 路由:使用 vue-router 等时,每次路由切换需重新 wx.config()
  • 上传图片chooseImage 获取的是 localId,需用 uploadImage 上传到微信服务器再下载

Hybrid 开发最佳实践

  • 版本控制:H5 资源加版本号,利用 WebView 缓存,避免每次加载
  • 降级策略:原生能力不可用时,优雅降级到 H5 实现(如 WebRTC、Geolocation API)
  • 离线包:将 H5 资源打包到 App 本地,通过离线包 + 增量更新提升加载速度
  • 安全校验:敏感操作(支付、授权)必须由原生完成,不可纯 H5 实现
  • Cookie 同步:Android WebView 默认不共享 Cookie,需要原生层做同步

Hybrid / JSBridge 避坑指南

坑点问题描述解决方案
JSBridge 注入时机不确定 原生注入的 Bridge 对象可能在页面 JS 执行之后才就绪,window.NativeBridgeundefined H5 端实现轮询等待机制(最大超时 5s),或原生在页面加载前注入(onPageStarted
回调 ID 冲突 并发调用多个原生方法时,回调函数可能互相覆盖,导致结果混乱 使用唯一 callbackId(时间戳 + 随机数),维护回调映射表而非全局变量
JSON 序列化深度嵌套 复杂嵌套对象通过 URL Scheme 传递时可能丢失数据或超出长度限制 优先使用注入方式传参;URL Scheme 方案限制参数层级 ≤ 2 层,单次数据 < 2KB
页面跳转导致回调丢失 调用原生方法后 H5 立即跳转页面,原生回调时 JS 上下文已销毁 跳转前等待回调完成;或原生将结果缓存到 localStorage,新页面读取
iOS WKWebView 跨域限制 WKWebView 中 AJAX 请求跨域被拦截(file:// 协议尤其严重) 原生端配置允许任意跨域;使用原生 HTTP 代理转发请求;H5 资源用本地服务器加载
Android WebView 内存泄漏 WebView 持有 Activity 引用,未正确销毁导致 Activity 无法回收,内存持续增长 单独进程运行 WebView;onDestroy 中先 removeAllViews()destroy();使用 ApplicationContext 创建 WebView
软键盘遮挡输入框 键盘弹出时 WebView 不会自动调整布局,H5 输入框被软键盘遮挡 原生端设置 android:windowSoftInputMode="adjustResize";H5 端监听 visualViewport 手动滚动到焦点元素
UserAgent 伪造风险 仅靠 UserAgent 判断 App 环境不可靠,攻击者可伪造 UA 绕过安全校验 结合 JSBridge 探活双重验证:UA 检测 + typeof window.NativeBridge !== 'undefined'
微信 JSSDK 签名失败 SPA 应用中路由切换后签名失效,或 iOS 和 Android 签名 URL 不一致 SPA 每次路由切换重新 wx.config();iOS 使用首次进入页面的 URL 签名,Android 使用当前 URL
Android 4.4 以下兼容性 Android 4.4 以下使用 WebKit 内核,不支持 ES6、Flexbox、CSS 变量等 确认最低支持版本,如需兼容则使用 Polyfill + Autoprefixer + ES5 转译;否则引导升级
WebView 白屏时间过长 H5 资源加载慢或网络差时,WebView 长时间白屏,用户体验差 原生先展示骨架屏/Loading;使用离线包优先加载;开启 WebView 缓存和预加载池
调试效率低 Hybrid 开发涉及两端,排查 JSBridge 通信问题需要同时调试 H5 和原生代码 Android: chrome://inspect 远程调试;iOS: Safari → 开发 → 选择设备;封装统一的 Bridge 日志开关,两端可打印通信日志