一、问题背景
最近在做混合开发(Hybrid)项目,需要让 H5 页面嵌入到 App 宿主,前端通过 JSBridge 与 native 通信,实现像安全区适配、界面横竖屏、关闭 webview 等常见能力。
原以为都是成熟技术方案,但在日志后台却发现大量报错,内容类似:
js
PROMISE_ERROR: Error.message: r is not a function. (In 'r("xxxCall",e,function(e){try{console.log("response:",e)}catch(e){console.log(e)}})', 'r' is undefined)
二、问题排查与临时解决方法
2.1 问题排查
分析代码和堆栈后,发现报错均集中在 JSBridge 调用 Native 能力的时刻。进一步排查发现:
- JSBridge 尚未挂载到 window 时即被调用,导致方法不存在("xxx is not a function")。
- 加载速度快的设备/网络环境、用户频繁刷新时更容易复现。
- 造成 JS-Native 通信链路断裂。
2.2 临时解决方案
为快速止损,我们实施了以下方案:
- 检测桥对象是否挂载,若未挂载,则 setTimeout 轮询多次尝试
- 每次尝试失败自动上报日志(便于定位)
- 规定最多重试2次,仍未成功则放弃调用
这种方案虽然能兜底,但实际问题不少:
- 重复轮询逻辑冗余,潜在性能损耗
- 可能还是有窗口期 race condition
- 对业务方约束力弱,易误用或遗漏
- 代码维护不方便,缺乏抽象
除了技术解决,也想借此机会理清 JSBridge 本身的机制,为后续彻底优化做铺垫。
三、原理科普:双端 JSBridge 初始化机制详解
3.1 为什么需要 JSBridge
Hybrid 模式下,前端页面运行在 WebView 容器中,许多重要能力通过 JSBridge 进行 JS<->Native 通信,包括:
- 主动调用 native 功能(安全区信息、关闭页面、支付、导航等)
- Native 反馈数据给 JS(事件回调、业务状态等)
3.2 iOS 端 JSBridge机制与初始化流程
iOS 平台主流 WebView (UIWebView/WKWebView)采用 WebViewJavascriptBridge 或类似实现。
桥的初始化流程:
- Native 在加载页面后,异步注入
WebViewJavascriptBridge
对象,并通过特定 scheme(https://__bridge_loaded__
)或消息来完成 JS 和 Native 的 handler 绑定。 - JS 侧通过注册回调监听
WVJBCallbacks
,只有 Native 主动释放桥 ready信号时,回调才会执行。 - 从 JS 的角度来看,桥对象 ready 的时机是不可预测、只能被动等待。
我们调用的JSBrige官方文档用法如下:
js
function setupWebViewJavascriptBridge(cb) {
if (window.WebViewJavascriptBridge) {
return cb(window.WebViewJavascriptBridge);
}
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(cb);
}
window.WVJBCallbacks = [cb];
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(iframe);
setTimeout(function() {
iframe.parentNode.removeChild(iframe);
}, 0);
}
结论:
iOS 环境下,所有 JSBridge 调用都必须等桥 ready,否则就是未定义、报错。
3.3 Android 端 JSBridge机制与初始化流程
Android WebView(或各大定制内核)通常通过 addJavascriptInterface
实现 JSB。
桥的初始化流程:
- Native 在页面加载生命周期(如 onPageStarted、onPageFinished)同步注入一个 JS 对象(如
window.android
、window.JSBridge
),提供方法给 JS 调用。 - JS 侧可以立即访问和调用,无需异步等待。
- 部分实现还支持通过
prompt
拦截实现更深度通信,但广义桥对象都是同步可用。
结论:
Android 通常桥一加载页面就 ready,极罕见 race condition。
3.4 各自特点对开发时机的影响
iOS | Android | |
---|---|---|
桥对象初始化 | 异步注入 需等回调 | 同步注入 可直接用 |
风险 | race condition,"未定义" | 基本无差错 |
推荐做法 | 桥 ready 事件/Promise控制 | 普通同步逻辑 |
四、高可靠桥 ready 改造:Promise 化初始化方案
4.1 方案思路
轮询不是长久之计,更优雅的解决是利用 Promise:
- 桥对象 ready 时通过 Promise resolve
- 业务调用方统一
await bridgeReady
,只处理安全已挂载后的调用 - Android 桥若同步,直接 resolve 即可
这种方案兼顾了异步/同步 ready 的差别,统一业务调用逻辑。
4.2 代码示意
js
let bridgeReadyResolve;
const bridgeReady = new Promise((resolve) => {
bridgeReadyResolve = resolve;
});
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(window.WebViewJavascriptBridge);
bridgeReadyResolve();
return;
}
if (window.WVJBCallbacks) {
window.WVJBCallbacks.push(function(bridge) {
callback(bridge);
bridgeReadyResolve();
});
return;
}
window.WVJBCallbacks = [function(bridge) {
callback(bridge);
bridgeReadyResolve();
}];
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(iframe);
setTimeout(function() {
iframe.parentNode.removeChild(iframe);
}, 0);
}
function initBridge() {
if (isiOS()) {
setupWebViewJavascriptBridge(function(bridge) {
window._callHandler = bridge.callHandler;
});
} else {
// Android 可直接桥挂载
bridgeReadyResolve();
}
}
// 实际调用jsb
async function runNativeMethod(methodStr) {
// 用Promise自动控制jsb的调用
await bridgeReady;
if (isiOS()) {
if (typeof window._callHandler === 'function') {
window._callHandler('NativeMethod', methodStr, function(resp) {
// 处理返回
});
}
} else {
if(window.android) {
window.android.nativeFunc(methodStr);
}
}
}