JSBridge
JSBridge 是 WebView 与原生应用(Native)之间的双向通信机制:JavaScript 可以调用原生功能,原生也可以调用 Web 端的 JavaScript 方法。
本质要解决的问题:Web 跑在沙箱里,不能直接碰相机、定位、打印等系统能力;Native 也不能直接操作页面 DOM。两端需要一条约定好的「管道」。
知识地图
ini
概念层 JSBridge = 通信架构 / 协议(不是某一段固定代码)
│
实现层 URL 拦截 · 代码注入 · MessageHandlers(WKWebView 通道)
│
库 / API JsBridge、DSBridge、微信 JS-SDK ...
│
调用层 callHandler / registerHandler 或 约定 URL + callbackId
建议阅读顺序:先分清「概念 ≠ 某个全局对象」→ 再看消息协议 → 再对比两种实现 → 最后看前端怎么写。
一、先分清三层:概念、库、入口对象
window.WebViewJavascriptBridge 不是 JSBridge 本身,只是开源库 WebViewJavascriptBridge 挂到页面上的调用入口。
| 名称 | 层级 | 含义 |
|---|---|---|
| JSBridge | 抽象概念 / 协议 | Web(JS)与 Native(OC / Java 等)双向通信的架构。URL Scheme 拦截、注入 API、MessageChannel 等能打通两端隔离的方案,都属于 JSBridge |
| WebViewJavascriptBridge | 具体开源库 | 封装 iOS / Android WebView 差异,注入脚本,暴露 callHandler、registerHandler |
window.WebViewJavascriptBridge |
调用入口 | 页面里实际调的那个全局对象 |
可类比为:
JSBridge ≈ HTTP;WebViewJavascriptBridge ≈ Axios;
callHandler≈axios.post()。
其他实现的通信原理相同,只是全局对象和方法名不同:
| 实现 | 全局对象 / 典型 API |
|---|---|
| WebViewJavascriptBridge | window.WebViewJavascriptBridge.callHandler() |
| 微信 JS-SDK | window.wx,如 wx.invoke()、wx.ready() |
| 支付宝 JS-API | AlipayJSBridge.call() |
| 大厂自研 | window.NativeBridge、window.AppInterface;方法也可能叫 send()、postMessage()、execute() |
二、通信协议(所有实现共用)
本质是事件驱动的异步通信 :两端按约定交换 JSON 消息,用 callbackId 把「请求」和「响应」配对。
| 规范 | 说明 |
|---|---|
| 命名空间 | 「模块:方法」,如 geo:getlocation |
| 参数编码 | JSON 传递结构化数据 |
| 回调标识 | callbackId 匹配请求与响应 |
每条消息通常包含三个字段:
| 字段 | 作用 |
|---|---|
handlerName |
要调用的方法名。接收方按这个名字查找本地是否 registerHandler 过 |
data |
业务参数 |
callbackId |
可选。有则处理完后按 ID 回传,对应发起方那次调用的回调 |
Web → Native
JS 生成 message 入队 → 触发 URL 变化或调用注入对象 → Native 拦截 / 分发 → 按 callbackId 返回结果。
Native → Web
Native 通过 evaluateJavaScript(或已注册的 Handler)执行 JS → 获取返回值或触发回调。
协议是对称的:无论谁发起,底层都是「按名字找处理器 + 按 ID 回传」。
三、两种实现方式
1. 总览
| 方式 | 怎么通 | 适合 |
|---|---|---|
| URL 拦截 | 构造约定 URL(常用隐藏 iframe 设 src),Native 拦截后解析 |
轻量、兼容优先、无需注入对象 |
| 代码注入 | Native 向页面注入 JS 对象,前端直接调方法 | 复杂参数、高频调用、原生主动推事件 |
| MessageHandlers | iOS WKWebView 的 window.webkit.messageHandlers |
属于注入 / 通道类,是 iOS 官方推荐通道 |
2. URL 拦截
优点
- 门槛低:不必改 WebView 内核或注入对象,拦截 URL 即可。
- 兼容性好:多数系统版本和 WebView 都能用。
- 安全性可控:约定协议 + 黑白名单 / URL 校验。
- 好调试:抓包即可看到通信内容。
缺点
- 能力受限:主要靠 URL 参数,难传大对象、函数。
- 性能开销大:每次通信一次 URL 请求,高频调用容易卡顿。
- 可能被绕过:Web 端用非常规方式构造请求时,拦截可能失效。
3. 代码注入
优点
- 能力强:注入原生对象,支持复杂数据、同步 / 异步回调。
- 性能好:内存里做方法映射,没有额外「伪网络请求」。
- 易扩展:可动态注入方法,封装更多原生能力。
缺点
- 实现复杂:要处理注入时机、系统版本、作用域隔离。
- 安全风险高:注入环境权限管控不严,可能被恶意脚本利用。
- 难调试:发生在 WebView 内部,常规抓包看不到过程。
4. 选型
- 轻量、低频:优先 URL 拦截,落地快、更稳。
- 复杂业务、高频、需要原生主动推事件:优先代码注入。
四、平台落地:Android 与 iOS
同一套协议,两端用不同 API 接。
Android
| 方式 | 说明 |
|---|---|
addJavascriptInterface |
4.2 以上需 @JavascriptInterface,把 Java 对象注入页面供 JS 调用(代码注入) |
| URL 拦截 | shouldOverrideUrlLoading 拦截 iframe.src 触发的请求 |
onJsPrompt 拦截 |
劫持 window.prompt 通信,避免占用 alert / console |
evaluateJavascript |
4.4 以后,Native 调 JS,效率更高且可拿到返回值 |
iOS
| 方式 | 说明 |
|---|---|
| WKWebView URL 拦截 | decidePolicyForNavigationAction(导航请求)或 decidePolicyForNavigationResponse(响应)拦截 URL |
messageHandlers |
window.webkit.messageHandlers 注册消息处理器 |
evaluateJavaScript |
Native 调 JS,支持异步回调 |
常见运行环境差异
- 微信:标准 JS-SDK(
wx.ready) - X5 内核:自定义协议(如
tbs://invoke) - 通用 WebView:有注入对象走代码注入,否则降级到 URL 拦截
五、常见库:JsBridge 与 DSBridge
两者都是对 JSBridge 协议的封装。JsBridge 走 callHandler / registerHandler;DSBridge 走自己的 dsBridge.call。
| 对比维度 | JsBridge | DSBridge |
|---|---|---|
| 代表仓库 | lzyzsd/JsBridge |
wendux/DSBridge-Android、DSBridge-IOS |
| 平台 | Android(H5 API 与 iOS 的 WebViewJavascriptBridge 对齐,两端可共用同一套 JS) | 官方 Android + iOS |
| 前端入口 | window.WebViewJavascriptBridge |
dsBridge |
| 调用风格 | 只有异步:callHandler / registerHandler |
同步 dsBridge.call(name, arg) + 异步(带 callback) |
| 额外能力 | 基础双向调用、回调匹配 | 命名空间、API Object、Progress Callback(一次调用多次回传) |
| 正式发版 | JitPack 常用 1.0.4 | iOS 3.0.6 (2018-11);npm dsbridge 3.1.4(2018-09) |
| 维护现状 | 官方很少动,有社区 fork | 约 2019 年后基本停更 |
选型:
- H5 已写
callHandler/registerHandler,只要基础双向调用 → JsBridge(iOS 用同 API 的 WebViewJavascriptBridge)。 - 需要同步返回值、命名空间、进度多次回传 → DSBridge。
- 两边官方都已停更;新项目更常见的是跟平台走(WKWebView
messageHandlers、AndroidJavascriptInterface)或自研薄封装。
六、前端怎么用
1. 先等待桥就绪
H5 脚本执行 和 Native 注入是异步 的。桥还没挂上就 callHandler 会报错。
常见写法:已存在则立刻用;否则监听 WebViewJavascriptBridgeReady。
javascript
/**
* 等待 Native 把桥注入完成后再执行业务回调。
*
* 坑:若 Native 已注入且 Ready 事件已经派发过,
* 后挂上的 addEventListener 收不到该事件。
* 只监听事件可能永久挂起;兜底做法是轮询检测
* window.WebViewJavascriptBridge 是否存在,并设超时。
*/
function setupWebViewJavascriptBridge(callback) {
// 桥已存在:Native 注入更早,立刻把桥交给回调
if (window.WebViewJavascriptBridge) {
return callback(window.WebViewJavascriptBridge);
}
// 桥还不存在:等 Native 注入完成后派发的就绪事件
document.addEventListener(
"WebViewJavascriptBridgeReady",
function () {
callback(window.WebViewJavascriptBridge);
},
false
);
}
| 等待策略 | 优点 | 缺点 |
|---|---|---|
| 只听 Ready 事件 | 几乎零延迟、零开销 | 可能漏掉已派发的事件,一直挂起 |
| 轮询全局对象 | 不依赖事件,能超时失败 | 最多多等一个 interval |
稳妥组合:先看对象在不在 → 再听事件 → 再用短轮询兜底。
2. 代码注入:callHandler(Web → Native)
Native 注入 window.WebViewJavascriptBridge 后,前端直接调对象方法。下面以「发起打印」为例。
javascript
setupWebViewJavascriptBridge(function () {
window.WebViewJavascriptBridge &&
window.WebViewJavascriptBridge.callHandler(
"printPaper", // 1. handlerName:原生已注册的方法名
JSON.stringify(e), // 2. data:传给原生的参数(须为字符串)
function (i) { // 3. callback:本次一次性回调,i 为 JSON 字符串
var a = JSON.parse(i);
200 == a.code ? e.success(a) : e.fail(a);
}
);
});
| 入参 | 本例值 | 含义 |
|---|---|---|
1. handlerName |
"printPaper" |
要调用的原生方法名,须与 Native 注册名一致 |
2. data |
JSON.stringify(e) |
桥通常只收字符串,对象要先序列化;无参数时常传 "" |
3. callback |
function (i) { ... } |
这一次 调用的回调。原生返回 JSON 字符串,按 code 走成功 / 失败 |
3. 代码注入:registerHandler(Native → Web)
前端先注册方法名,原生在任意时刻(打印完成、缺纸、进度)主动通知页面。
javascript
setupWebViewJavascriptBridge(function () {
window.WebViewJavascriptBridge &&
window.WebViewJavascriptBridge.registerHandler(
"paperPrinterCallback", // 1. 前端暴露给原生的名字
function (i, a) { // 2. 原生来调时执行
var n = JSON.parse(i);
e.success(n);
}
);
});
| 入参 | 本例值 | 含义 |
|---|---|---|
1. handlerName |
"paperPrinterCallback" |
前端暴露给原生的方法名 |
2. handler |
function (i, a) |
长期有效。i 是原生传入的 JSON 字符串;a 是可选的 responseCallback,处理完可再回传给原生 |
本例双向对照:
| API | 方法名 | 方向 | 含义 |
|---|---|---|---|
callHandler |
printPaper |
Web → Native | 前端主动发起打印 |
registerHandler |
paperPrinterCallback |
Native → Web | 接收原生后续推送(可多次) |
4. 为什么 callHandler 已有回调,还要 registerHandler?
两者不能互相替代。差别在三件事:谁主动、活多久、能否解耦。
结合打印:printPaper 的第三个参数只能回答「这次指令有没有被原生接住」;真正打完、缺纸、进度,由原生在任意时刻推过来,必须靠事先注册的 paperPrinterCallback。
① 通信方向
callHandler:H5 主动调原生(相机、定位、打印)。回调只收这一次结果,返回后这次通信结束。- 局限:原生若要主动 通知 H5(登录态变化、推送、从后台回前台、打印完成),当时往往没有正在进行的
callHandler,也就没有回调可塞。H5 必须提前registerHandler。 - 对称点:JS 能
callHandler('printPaper'),前提通常是 Native 已经注册了同名 Handler;没注册,Native 不知道跑哪段代码。
② 生命周期
callHandler的回调是一次性的:存在以callbackId为键的临时字典里,执行后删除。再点一次按钮要重新callHandler。registerHandler是持久的:页面不刷新就一直在,原生可多次触发。例如「摇一摇」应在加载时registerHandler('onShake', callback),而不是每次摇晃都等 H5 先发一次callHandler。
③ 异步解耦
原生收到 WebSocket 推送、要更新 H5 红点时,H5 并没有发起请求,原生手里没有回调引用。正确做法:
- H5 先
registerHandler('updateBadge', function (count) { ... }) - 原生收到推送后主动
callHandler('updateBadge', count)(此时原生是发起方)
对照(记住这一张表即可)
| 特性 | callHandler 里的回调 |
registerHandler |
|---|---|---|
| 触发时机 | 发起方调用后,对方处理完毕时 | 对方随时可触发(知道 handlerName 即可) |
| 生命周期 | 一次性,执行后销毁 | 持久,直到页面销毁或手动移除 |
| 主要用途 | 单次操作结果(支付、指令是否被接收) | 暴露接口;监听对方主动事件 |
| 主动性 | 被动接收本次结果 | 能力提供方 / 长期监听 |
| 本例 | printPaper 的第三参 |
paperPrinterCallback |
结论:
callHandler的回调:让发起方 拿到本次操作的结果。registerHandler:让接收方 暴露能力,方便对方在任意时刻主动调用。
仅靠一次性回调,做不了原生对 H5 的主动通知和复杂状态同步。
5. URL 拦截写法(对照)
不依赖注入对象,用隐藏 iframe 发约定 URL,避免页面跳转。
javascript
function callNativeByUrl(module, method, params, callbackId) {
const query = [
"params=" + encodeURIComponent(JSON.stringify(params || {})),
"callbackId=" + encodeURIComponent(callbackId || "")
].join("&");
// 约定协议:jsbridge://模块/方法?params=...&callbackId=...
const url = "jsbridge://" + module + "/" + method + "?" + query;
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.src = url;
document.documentElement.appendChild(iframe);
setTimeout(function () {
iframe.parentNode && iframe.parentNode.removeChild(iframe);
}, 0);
}
callNativeByUrl("geo", "getlocation", { timeout: 5000 }, "cb_1001");
callNativeByUrl("nav", "backHome", {}, "cb_1002");
原生处理完后,用 evaluateJavaScript 调前端按 callbackId 挂好的函数:
javascript
window.JSBridgeCallbacks = window.JSBridgeCallbacks || {};
window.JSBridgeCallbacks.cb_1001 = function (response) {
console.log("定位结果", response);
};
// Native 侧等价:
// window.JSBridgeCallbacks.cb_1001({"code":200,"data":{"lat":36.7,"lng":119.1}})
七、一张表收口
| 对比项 | 代码注入(callHandler / registerHandler) |
URL 拦截 |
|---|---|---|
| 入口 | window.WebViewJavascriptBridge 这类注入对象 |
隐藏 iframe / 改 location 为约定 URL |
| 传参 | JSON 字符串或对象 | URL query,需 encodeURIComponent |
| 回调 | callHandler 第三参;registerHandler 的 responseCallback |
前端按 callbackId 挂函数,Native evaluateJavaScript 触发 |
| Native 主动推事件 | 靠 H5 事先 registerHandler |
同样靠预先挂好的全局回调函数 |
| 适用 | 复杂参数、高频、双向事件 | 轻量、兼容优先、无需注入对象 |
记忆口诀
- JSBridge 是协议,某个
window.xxx只是入口。 - 消息三件套:
handlerName+data+callbackId。 - 实现两条路:URL 拦截(兼容) / 代码注入(能力与性能)。
callHandler问「这次怎么样」;registerHandler说「以后有事找我」。