移动端 H5 页面跑在 WebView 里,它天然只能访问浏览器提供的能力。但很多业务页面并不满足于普通 Web 能力:需要打开客户端登录、获取设备信息、调起分享面板、打开相册、跳转原生页面、读取定位、上报客户端埋点。
这些能力都在客户端里,H5 不能直接调用。JS Bridge 要解决的就是这个问题:在 JS 和 Native 之间建立一条通信通道。
它看起来像是前端调用一个普通函数:
bridge.call("share.openPanel", { title: "新车上市", url: location.href});
但实际过程是:
H5 发起调用 -> Bridge 序列化调用信息 -> WebView 把消息传给客户端 -> 客户端解析协议并执行原生能力 -> 客户端把结果回传给 H5 -> Bridge 找到对应 callback 并 resolve
JS Bridge 的难点不在“能不能发消息”,而在于这条链路是否稳定、可追踪、可兼容、可降级。
WebView 和客户端之间有哪些通信方式
不同平台、不同 WebView 容器支持的通信方式不完全一样,但常见方案可以分成几类。
第一类是 URL Scheme 拦截。
H5 创建一个特殊协议的 URL:
location.href = "myapp://bridge?method=share.openPanel¶ms=...";
客户端在 WebView 的 URL 加载回调中拦截这个 URL,发现协议是 myapp://bridge,就不真的跳转,而是解析里面的方法和参数。
这种方案兼容性较好,早期很多 Bridge 都这么做。但缺点也明显:URL 长度有限,参数需要编码,连续调用时容易丢消息,也不适合传大数据。
第二类是注入 Native 对象。
Android WebView 可以通过 addJavascriptInterface 暴露对象:
window.NativeBridge.invoke(JSON.stringify(message));
客户端实现 NativeBridge.invoke,收到 JS 传过来的字符串后解析执行。
这种方式调用直接,但要考虑安全问题。暴露给 JS 的方法必须非常克制,不能把过多原生能力直接挂到全局对象上。
第三类是 WebKit MessageHandler。
iOS WKWebView 可以通过 window.webkit.messageHandlers 发消息:
window.webkit.messageHandlers.NativeBridge.postMessage(message);
这是比较现代的方式,语义清晰,也比 URL Scheme 更适合结构化消息。
第四类是客户端执行 JS 回调。
无论 H5 通过哪种方式把消息发给客户端,客户端最终都需要把结果通知给 H5。常见方式是客户端执行一段 JS:
window.__bridge__.handleNativeResponse({ callbackId: "cb_1001", code: 0, data: {}});
这一步是 Bridge 能支持异步调用的关键。
一次完整调用应该有 requestId
Bridge 调用不能只传方法名和参数,还必须有唯一 ID。
原因很简单:客户端能力大多是异步的。比如登录、分享、定位、选择图片,都不可能立即同步返回。H5 发出多个请求以后,客户端回来的结果必须能对应到具体请求。
一个标准消息可以这样设计:
{ "id": "bridge_10001", "method": "share.openPanel", "params": { "title": "新车上市", "url": "https://example.com" }, "timestamp": 1536900000000}
H5 侧保存 callback:
const callbacks = new Map();function call(method, params) { const id = createBridgeId(); const promise = new Promise((resolve, reject) => { callbacks.set(id, { resolve, reject }); }); sendToNative({ id, method, params, timestamp: Date.now() }); return promise;}
客户端执行完成后回传:
{ "id": "bridge_10001", "code": 0, "message": "ok", "data": { "shared": true }}
H5 根据 id 找到对应 Promise:
window.__bridge__.handleResponse = function(response) { const callback = callbacks.get(response.id); if (!callback) return; callbacks.delete(response.id); if (response.code === 0) { callback.resolve(response.data); } else { callback.reject(response); }};
没有 requestId 的 Bridge 只能处理简单同步指令,一旦遇到并发调用、异步回调、超时和失败重试,就会很难维护。
Bridge SDK 要屏蔽平台差异
业务页面不应该关心当前运行在 Android 还是 iOS,也不应该知道底层是 URL Scheme、MessageHandler 还是注入对象。
前端应该只面向统一 SDK:
await bridge.call("user.getLoginInfo");await bridge.call("navigation.openPage", { url: "app://car/detail?id=123" });await bridge.call("share.openPanel", { title, url });
Bridge SDK 内部再判断平台:
function sendToNative(message) { if (isIOS() && window.webkit?.messageHandlers?.NativeBridge) { window.webkit.messageHandlers.NativeBridge.postMessage(message); return; } if (isAndroid() && window.NativeBridge?.invoke) { window.NativeBridge.invoke(JSON.stringify(message)); return; } fallbackByScheme(message);}
这样业务代码不会被平台差异污染。后续客户端切换 WebView 实现,或者新增一种通信方式,也只需要升级 Bridge SDK。
客户端主动通知也要进入 Bridge 体系
Bridge 不只是 H5 调 Native,也包括 Native 主动通知 H5。
例如:
客户端登录状态变化原生页面返回后通知 H5 刷新网络状态变化App 从后台回到前台客户端主题切换
这些更适合做成事件机制:
bridge.on("app.resume", () => { refreshPageData();});bridge.on("user.loginChange", user => { updateUserInfo(user);});
客户端触发事件时执行:
window.__bridge__.emit("user.loginChange", { isLogin: true, userId: "123"});
Bridge SDK 统一分发事件。这样业务页面不需要暴露一堆全局函数给客户端调用,也能避免函数名冲突。
超时和降级不能省
Bridge 调用一定要考虑失败。
失败来源很多:
- 当前页面不在客户端内。
- 客户端版本太低,不支持某个方法。
- WebView 注入时机晚于页面调用。
- 用户取消操作,例如取消分享或取消登录。
- 客户端执行异常,没有回调。
- 网络或权限原因导致原生能力失败。
所以 SDK 必须给每次调用设置超时:
function call(method, params, options = {}) { const timeout = options.timeout || 5000; const id = createBridgeId(); return new Promise((resolve, reject) => { const timer = setTimeout(() => { callbacks.delete(id); reject({ code: "BRIDGE_TIMEOUT", message: `${method} timeout` }); }, timeout); callbacks.set(id, { resolve: data => { clearTimeout(timer); resolve(data); }, reject: error => { clearTimeout(timer); reject(error); } }); sendToNative({ id, method, params }); });}
同时,业务层要有降级方案。比如客户端分享不可用时,可以展示 Web 分享提示;客户端登录不可用时,可以跳转 Web 登录;客户端定位失败时,可以让用户手动选择城市。
Bridge 不能假设自己永远可用。一个成熟的 SDK 一定会把不可用当作常态处理。
安全边界也要设计清楚
JS Bridge 打通了 H5 和客户端能力,如果没有边界,很容易变成安全风险。
至少要考虑:
- 哪些域名允许调用 Bridge。
- 哪些方法只允许可信页面调用。
- 敏感能力是否需要用户确认。
- 参数是否需要客户端再次校验。
- 是否允许第三方页面打开客户端内部页面。
- 是否允许 H5 获取设备、登录、定位等敏感信息。
客户端不能因为 H5 传了一个 method 就直接执行。Bridge 协议层需要白名单:
trusted domains -> allowed methods -> parameter validation -> permission check
前端也不能把 Bridge 当成万能接口。能用普通 Web 能力解决的问题,就不要强依赖客户端能力;必须调用客户端时,也要考虑用户权限和失败路径。
小结
JS Bridge 的本质是 WebView 与客户端之间的一套通信协议。它不是简单挂一个全局函数,也不是随便发一个 URL Scheme。
一个可靠的 Bridge 至少要具备:
- 统一的调用入口。
- 跨平台的底层通信适配。
- requestId 与异步回调管理。
- 客户端主动事件通知。
- 超时、错误和降级处理。
- 方法白名单和安全校验。
移动端 H5 和客户端交互越多,Bridge 的工程价值越明显。它把零散的端能力调用收束成统一协议,让业务页面既能使用客户端能力,又不被平台差异和版本差异拖垮。