🔗 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.NativeBridge 为 undefined |
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 日志开关,两端可打印通信日志 |