JSBridge 原理与实践
问题
什么是 JSBridge?它是如何在 WebView 与 Native 之间双向通信的?iOS 和 Android 的实现有什么区别?如何设计一个健壮、可扩展的 JSBridge?
什么是 JSBridge? JSBridge 是 Hybrid App 里H5 跟 Native 讲话的「跨语言 RPC」:
- JS 包装一个
{handlerName, params, callbackId}的消息发给 Native。 - Native 拿到 callbackId 调用对应原生能力,完成后通过
callbackId回调 JS。 - 本质是一套约定。
JS 怎么调 Native? 有四种常见方式,现代项目以后两种为主:
- 拦截 URL Scheme:JS 创建
iframe加载myapp://method?params=xxx,Native 在路由拦截里识别。兼容性最好但现在用得少。 - 拦截
prompt/console:业务中已淘汰。 - 注入 API:Android 用
addJavascriptInterface(obj, "NativeBridge")把对象注入 window;JS 直接调window.NativeBridge.method()。需要@JavascriptInterface注解防必须调反射漏洞。 - MessageHandler(iOS WKWebView 主流):JS 调
window.webkit.messageHandlers.bridge.postMessage(payload),注册了WKScriptMessageHandler的 Native 代码会收到消息。
Native 怎么回调 JS? 只有一种本质方式:执行 JS 代码。
- Android:
webView.evaluateJavascript("window.bridge.callback('id', result)", null)。 - iOS:
webView.evaluateJavaScript(...)。 - 实际框架会维护一个
callbackId → fn的 Map,回调后查表调 JS 函数。
如何设计一个健壮的 JSBridge? 几个必须考虑的点:
- 统一协议:请求/响应结构一致(
{handlerName, params, callbackId, code, data})。 - 超时 + 错误处理:防止 Native 没回调导致 Promise 永远 pending。
- 权限白名单:仅官方域名能调敏感 API(防中间人劫持 H5 调原生能力)。
- 版本協商:Native API 升级后 H5 需能检查能力是否存在,有降级方案。
答案
JSBridge 是 Hybrid App(混合应用)中连接 JavaScript 与 Native(iOS/Android) 的通信桥梁。它让运行在 WebView 中的 H5 页面能够调用 Native 能力(拍照、定位、扫码、支付、登录态等),同时也让 Native 能够主动通知 H5(页面生命周期、推送消息、网络变化等)。
JSBridge 的本质是 "约定一套跨语言的 RPC 协议":JS 把"调用方法名 + 参数 + 回调 ID"按约定格式发送给 Native,Native 执行后再通过约定的方式把结果回调给 JS。
- JS → Native 通信主要有 4 种方式:注入 API、拦截 URL Scheme、拦截 prompt/console、MessageHandler
- Native → JS 只有一种本质方式:执行 JS 代码(
evaluateJavascript/evaluateJavaScript) - 现代主流方案:Android 用
addJavascriptInterface,iOS 用WKScriptMessageHandler - 设计核心:统一协议 + 回调管理(callbackId) + 超时/错误处理 + 权限白名单
为什么需要 JSBridge?
H5 页面运行在 WebView 沙箱中,受同源策略、浏览器 API 能力和系统权限模型限制,很多设备能力无法稳定、完整地访问:
| 能力 | 浏览器 H5 | Hybrid + JSBridge |
|---|---|---|
| 拍照/相册 | 可用 <input type="file">,但交互和后处理能力有限 | 调原生相机,可压缩、裁剪 |
| 定位 | 可用 Geolocation,但权限链路、后台定位和稳定性受限 | 原生定位(GPS/基站/Wi-Fi 融合) |
| 扫码 | 可用摄像头 + JS 识别,但兼容性、性能和权限体验不稳定 | 调原生扫码,识别速度快 |
| 推送 | Web Push(iOS 支持差) | APNs / FCM / 厂商通道 |
| 登录态 | Cookie/Storage | 共享 Native Token、生物识别 |
| 文件存储 | IndexedDB(容量受限) | 原生文件系统 |
| 蓝牙/NFC | 部分浏览器支持 | 完整原生 API |
Hybrid 架构(H5 + JSBridge + Native 容器)是前端跨端开发的基石——微信公众号页面、App 内 H5 活动页、小程序 web-view 页面等场景,本质上都依赖宿主容器与 Web 内容之间的 Bridge 能力。小程序本身则更接近宿主 App 提供的双线程运行时,不等同于普通 H5 Hybrid。
JSBridge 通信原理
双向通信模型
JS 调用 Native 的 4 种方式
1. 注入 API(推荐,现代主流)
Native 在 WebView 加载时直接向 window 注入一个对象,JS 即可同步/异步调用:
// Android:通过 addJavascriptInterface 注入
class JsBridge(private val webView: WebView) {
@JavascriptInterface // 必须加,否则 4.2+ 不会暴露
fun postMessage(message: String) {
// message 是 JSON 字符串:{method, params, callbackId}
val data = JSONObject(message)
// ... 路由到对应的原生处理器
}
}
webView.addJavascriptInterface(JsBridge(webView), "NativeBridge")
// JS 端调用:window.NativeBridge.postMessage(JSON.stringify({...}))
// iOS:通过 WKScriptMessageHandler 接收消息
let config = WKWebViewConfiguration()
config.userContentController.add(self, name: "nativeBridge")
// JS 端调用:window.webkit.messageHandlers.nativeBridge.postMessage({...})
func userContentController(_ uc: WKUserContentController,
didReceive message: WKScriptMessage) {
guard message.name == "nativeBridge",
let body = message.body as? [String: Any] else { return }
// 路由处理
}
优点:性能高、API 直观、支持复杂数据结构
缺点:Android 4.2 之前的 addJavascriptInterface 有严重的 RCE 漏洞(已淘汰)
2. 拦截 URL Scheme(兼容性最好,已少用)
JS 通过创建一个不可见的 iframe 设置 src 为自定义协议:
function callNative(method: string, params: object) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
// 自定义 scheme,如 jsbridge://getLocation?params=xxx
iframe.src = `jsbridge://${method}?params=${encodeURIComponent(JSON.stringify(params))}`;
document.body.appendChild(iframe);
setTimeout(() => document.body.removeChild(iframe), 0);
}
Native 通过拦截 shouldOverrideUrlLoading(Android)/ decidePolicyFor navigationAction(iOS)捕获该请求并解析参数。
优点:兼容性极佳(古早 WebView 也支持) 缺点:URL 长度限制(约 2KB)、性能差、连续调用会被合并丢失
3. 拦截 prompt / console / alert(特殊场景)
Android 早期为了规避 addJavascriptInterface 漏洞,曾用 WebChromeClient.onJsPrompt 拦截 window.prompt() 调用:
const result = prompt('jsbridge://getLocation', JSON.stringify(params));
iOS 上 UIWebView 时代也常用 JavaScriptCore + 拦截 console.log 实现。现代项目几乎不再使用。
4. WKWebView 的 messageHandlers(iOS 主流)
即上文的方式 1,是 iOS WKWebView 取代 UIWebView 后的官方方案。
Native 调用 JS 的方式
无论 iOS 还是 Android,本质都是一个:让 WebView 执行一段 JS 代码:
// Android(API 19+ 推荐 evaluateJavascript,可拿到返回值)
webView.evaluateJavascript("WebViewBridge.dispatch('onPush', ${jsonData})") { result ->
// 拿到 JS 返回值
}
// iOS
webView.evaluateJavaScript("WebViewBridge.dispatch('onPush', \(jsonData))") { result, error in
// ...
}
JS 端则在 window 上挂载一个统一的入口(如 WebViewBridge.dispatch)来分发事件。
通用 JSBridge 协议设计
一个生产级 JSBridge 应该有统一的消息格式:
// JS → Native 请求
interface BridgeRequest {
method: string; // 方法名,如 'device.getInfo'
params?: any; // 参数
callbackId: string; // 回调 ID(唯一),Native 完成后回传
}
// Native → JS 响应
interface BridgeResponse {
callbackId: string; // 与请求对应
code: number; // 0 成功,非 0 失败
message?: string; // 错误描述
data?: any; // 业务数据
}
// Native → JS 主动事件
interface BridgeEvent {
event: string; // 事件名,如 'app.onResume'
data?: any;
}
完整 JS 端实现
下面是一个生产可用的 JS 端 Bridge 封装,支持:Promise 化调用、回调管理、事件订阅、超时控制、iOS/Android 平台兼容。
type Callback = (res: BridgeResponse) => void;
type EventHandler = (data: any) => void;
class WebViewBridge {
// 回调表:callbackId → 回调函数
private callbacks = new Map<string, Callback>();
// 事件监听表:event → handlers
private events = new Map<string, Set<EventHandler>>();
private callbackSeed = 0;
constructor() {
// 暴露给 Native 调用的全局入口
(window as any).WebViewBridge = {
invokeCallback: this.invokeCallback.bind(this),
dispatch: this.dispatch.bind(this),
};
}
/** JS 调用 Native(Promise 化) */
call<T = any>(method: string, params?: any, timeout = 10000): Promise<T> {
return new Promise((resolve, reject) => {
const callbackId = `cb_${++this.callbackSeed}_${Date.now()}`;
// 超时控制:避免 Native 不回调时 Promise 永久挂起
const timer = setTimeout(() => {
this.callbacks.delete(callbackId);
reject(new Error(`[JSBridge] ${method} timeout`));
}, timeout);
// 注册回调
this.callbacks.set(callbackId, (res) => {
clearTimeout(timer);
if (res.code === 0) resolve(res.data);
else reject(new Error(res.message || 'Native error'));
});
// 平台判断 + 发送消息
const payload = { method, params, callbackId };
const cleanupAndReject = (error: Error) => {
clearTimeout(timer);
this.callbacks.delete(callbackId);
reject(error);
};
try {
if ((window as any).webkit?.messageHandlers?.nativeBridge) {
// iOS WKWebView
(window as any).webkit.messageHandlers.nativeBridge.postMessage(payload);
} else if ((window as any).NativeBridge?.postMessage) {
// Android:JSON 字符串化(Java 端只接收基本类型)
(window as any).NativeBridge.postMessage(JSON.stringify(payload));
} else {
cleanupAndReject(new Error('[JSBridge] Native bridge not available'));
}
} catch (err) {
cleanupAndReject(err instanceof Error ? err : new Error(String(err)));
}
});
}
/** Native 回调入口 */
private invokeCallback(callbackId: string, response: BridgeResponse) {
const cb = this.callbacks.get(callbackId);
if (cb) {
cb(response);
this.callbacks.delete(callbackId); // 一次性回调,调用后清理
}
}
/** 订阅 Native 事件 */
on(event: string, handler: EventHandler) {
if (!this.events.has(event)) this.events.set(event, new Set());
this.events.get(event)!.add(handler);
return () => this.events.get(event)?.delete(handler); // 返回取消订阅函数
}
/** Native 事件分发入口 */
private dispatch(event: string, data: any) {
this.events.get(event)?.forEach((h) => h(data));
}
}
export const bridge = new WebViewBridge();
// 业务调用示例
const location = await bridge.call<{ lat: number; lng: number }>('getLocation');
const off = bridge.on('app.onResume', () => console.log('App 回到前台'));
Android 端实现要点
class JsBridge(private val webView: WebView, private val context: Context) {
private val handlers = mutableMapOf<String, BridgeHandler>()
init {
// 注册原生能力
register("getLocation", LocationHandler(context))
register("scanQRCode", ScanHandler(context))
}
@JavascriptInterface // 关键:必须标注
fun postMessage(message: String) {
try {
val req = JSONObject(message)
val method = req.getString("method")
val params = req.opt("params") // params 可能是对象、数组、字符串或数字,由 handler 自己校验
val callbackId = req.getString("callbackId")
// 权限校验(白名单)
if (!isAllowed(method, webView.url)) {
callback(callbackId, 403, "Method not allowed")
return
}
// 异步执行(避免阻塞 JS 线程)
handlers[method]?.handle(params) { code, message, data ->
callback(callbackId, code, message, data)
} ?: callback(callbackId, 404, "Method not found")
} catch (e: Exception) {
Log.e("JsBridge", "postMessage failed", e)
// 如果已拿到 callbackId,应尽量回调统一错误;否则只能依赖 JS 侧超时兜底
}
}
private fun callback(id: String, code: Int, message: String?, data: Any? = null) {
val response = JSONObject().apply {
put("callbackId", id)
put("code", code)
put("message", message)
put("data", data)
}
// 关键:必须在主线程执行 evaluateJavascript
val callbackId = JSONObject.quote(id)
val script = "window.WebViewBridge.invokeCallback($callbackId, $response)"
webView.post {
webView.evaluateJavascript(script, null)
}
}
}
@JavascriptInterface注解是 Android 4.2+ 必需的安全防护,否则方法不会暴露给 JS- Android 4.2 以下的
addJavascriptInterface存在 任意代码执行漏洞(CVE-2012-6636),因此minSdkVersion >= 17是行业底线 - 必须做 URL 白名单校验,仅允许可信域名的页面调用敏感 API
iOS 端实现要点
class JsBridge: NSObject, WKScriptMessageHandler {
private weak var webView: WKWebView?
private var handlers: [String: BridgeHandler] = [:]
init(webView: WKWebView) {
super.init()
self.webView = webView
let ucc = webView.configuration.userContentController
ucc.add(self, name: "nativeBridge")
registerHandlers()
}
func userContentController(_ uc: WKUserContentController,
didReceive message: WKScriptMessage) {
guard message.name == "nativeBridge",
let body = message.body as? [String: Any],
let method = body["method"] as? String,
let callbackId = body["callbackId"] as? String else { return }
let params = body["params"] as? [String: Any]
// 权限校验
guard isAllowed(method: method, url: webView?.url) else {
callback(callbackId, code: 403, message: "Method not allowed")
return
}
handlers[method]?.handle(params) { [weak self] code, message, data in
self?.callback(callbackId, code: code, message: message, data: data)
}
}
private func callback(_ id: String, code: Int, message: String?, data: Any? = nil) {
let response: [String: Any] = [
"callbackId": id, "code": code,
"message": message ?? "", "data": data ?? NSNull()
]
guard let json = try? JSONSerialization.data(withJSONObject: response),
let str = String(data: json, encoding: .utf8) else { return }
guard let idJson = try? JSONSerialization.data(withJSONObject: [id]),
let idArray = String(data: idJson, encoding: .utf8) else { return }
let safeId = String(idArray.dropFirst().dropLast())
let script = "window.WebViewBridge.invokeCallback(\(safeId), \(str))"
DispatchQueue.main.async {
self.webView?.evaluateJavaScript(script)
}
}
}
WKUserContentController.add(self, name:) 会强引用 self,导致 WebView 和持有它的 ViewController 无法释放。
解决方案:使用一个弱引用代理对象转发消息,或在 ViewController deinit 前主动调用 removeScriptMessageHandler(forName:)。
安全设计
JSBridge 是攻击面最大的前端组件之一,必须严格防护:
| 风险 | 防护措施 |
|---|---|
| 任意页面调用敏感 API(拍照、支付) | URL 白名单 + 域名校验,仅可信域名可调用 |
| XSS 注入恶意 JS 调用 Bridge | H5 端做 CSP;敏感操作要求二次确认 |
addJavascriptInterface RCE 漏洞 | Android 必须 minSdkVersion >= 17 + @JavascriptInterface |
| 中间人攻击劫持 H5 页面 | 仅允许 HTTPS、SSL Pinning |
| Bridge 方法被反编译枚举 | 敏感方法加签名校验、设备绑定 Token |
| iOS WebView 缓存被恶意污染 | 配置 WKWebsiteDataStore.nonPersistent() |
权限分级与 URL 校验
生产环境不要只判断“域名是否在白名单”。一次 Bridge 调用至少应同时校验:
- URL 完整性:校验
scheme、host、端口和必要路径,避免把https://trusted.com.evil.com误判为可信域名 - 能力分级:把 API 分为普通能力、隐私能力、交易/支付能力等等级,高风险能力需要登录态、签名、用户确认或服务端二次授权
- 调用上下文:区分线上页面、离线包页面、内嵌第三方页面、调试页面,不同上下文暴露不同能力
- 参数校验:Native 端必须重新校验参数类型、范围和业务权限,不能信任 H5 传入的数据
主流开源 JSBridge 方案对比
社区有几个经典的 JSBridge 开源实现,各有特点:
| 方案 | 维护方 | 协议方式 | 平台支持 | 同步调用 | 适用场景 |
|---|---|---|---|---|---|
| WebViewJavascriptBridge | marcuswestin | iframe + 拦截 URL Scheme | iOS / Android | ❌ | 老项目兼容、UIWebView 时代 |
| JsBridge (lzyzsd) | lzyzsd | 拦截 URL Scheme(loadUrl) | Android | ❌ | Android 早期项目 |
| DSBridge | wendux | 注入 API(Android @JavascriptInterface、iOS WKScriptMessageHandler) | iOS / Android | ✅(同步调用) | 现代项目首选,跨三端 API 统一 |
| JSBridge (Hybrid SDK) | 各大厂自研 | 注入 API + 协议层定制 | iOS / Android | ✅ | 企业级超级 App(手淘、京东、字节) |
DSBridge 的设计亮点
DSBridge 是目前 GitHub 上最成熟的开源 JSBridge 之一,几个关键设计值得借鉴:
- 同步 + 异步双模式:JS 端可声明同步调用
bridge.call('xxx', params)直接拿返回值,也可异步bridge.call('xxx', params, callback) - 命名空间:方法名支持
namespace.method形式,避免方法名冲突 - JS API 自动注册:原生端通过反射自动暴露所有
@JavascriptInterface方法 - Native 主动调用 JS 也支持回调:
callHandler('jsMethod', args, returnValue -> {...})
import dsBridge from 'dsbridge';
// 同步调用(Android 通过 prompt 拦截、iOS 通过 messageHandlers + 内置同步等待)
const deviceId = dsBridge.call('device.getId');
// 异步调用
dsBridge.call('user.login', { phone: '138xxxx' }, (result) => {
console.log('登录结果', result);
});
// 注册 JS 方法供 Native 调用
dsBridge.register('refresh', () => {
location.reload();
});
"同步调用"在 Android 上是用 prompt 拦截实现的(阻塞 JS 主线程等待 Native 返回)。长耗时操作绝对不能用同步调用,否则页面假死。同步调用仅适合纯计算、读取本地缓存等毫秒级操作。
自研 vs 开源选择
| 选择 | 适合场景 |
|---|---|
| 直接用开源(DSBridge) | 中小型项目、内部工具、Hybrid 实验 |
| Fork + 二次封装 | 中大型项目,需要加监控、权限、降级等定制能力 |
| 完全自研 | 超级 App、有跨业务复用需求、安全要求极高 |
Hybrid 框架的 JSBridge 实现剖析
业界主流 Hybrid 框架(Cordova、Ionic Capacitor、uni-app、Taro)背后都有一套精巧的 JSBridge 实现。理解它们能帮助你设计自己的 Bridge。
Cordova(PhoneGap)—— 元老级 Hybrid 框架
Cordova 诞生于 2009 年,核心架构是 Plugin 机制 + JSBridge。
通信方式:
- iOS:早期 UIWebView 时用
iframe.src = 'gap://...'拦截 URL Scheme;WKWebView 后改为messageHandlers - Android:
addJavascriptInterface+prompt双备份(兼容老设备)
架构特点:
核心 API cordova.exec:
cordova.exec(
successCallback, // 成功回调
errorCallback, // 失败回调
'Camera', // Plugin 服务名
'takePicture', // Action(方法名)
[options] // 参数数组
);
每个 Plugin 用 plugin.xml 注册映射关系,原生端通过反射调用对应方法。这种"配置 + 反射"的设计让 Cordova 拥有了庞大的插件生态(曾有 2000+ 官方插件)。
衰落原因:
- 性能瓶颈(旧 WebView 渲染差)
- 插件维护质量参差不齐
- 新一代框架(Capacitor、RN)更现代
Capacitor —— Ionic 团队推出的 Cordova 继任者
Capacitor 2018 年由 Ionic 团队推出,目标是取代 Cordova。它的 JSBridge 设计更现代化:
核心改进:
| 维度 | Cordova | Capacitor |
|---|---|---|
| 通信底层 | URL Scheme 拦截 + addJavascriptInterface | postMessage / messageHandlers |
| 插件 API | 回调风格 | Promise / async-await |
| 类型支持 | JS 为主 | TypeScript 优先,自带类型定义 |
| Web 平台 | 不支持 | 支持(同一份代码可跑 Web、iOS、Android) |
| Native 工程 | 隐藏在 CLI 后 | 暴露原生工程(Xcode、Android Studio 直接打开) |
典型用法:
import { Camera, CameraResultType } from '@capacitor/camera';
// 全 Promise,无回调地狱
const image = await Camera.getPhoto({
quality: 90,
resultType: CameraResultType.Uri,
});
底层实现:每个 Plugin 在原生端通过 @CapacitorPlugin 注解暴露方法,自动生成 TypeScript 类型定义。JS 调用通过 Capacitor.nativeBridge.postMessage 转发到原生端的 MessageHandler,再路由到具体 Plugin。
uni-app / Taro —— 编译时跨端
国内的 uni-app(DCloud)和 Taro(京东)走的是另一条路线——编译时把 Vue/React 代码转成各平台原生代码:
- 编译到 小程序:转成 wxml + wxss + js(小程序自带 JSBridge)
- 编译到 H5:直接跑在浏览器
- 编译到 App(uni-app 的 nvue / Taro 的 RN 模式):转成原生组件
它们如何处理 JSBridge?
- 在 App 端,封装统一的跨端 API:
uni.scanCode()/Taro.scanCode() - 编译时根据目标平台替换为对应实现:小程序调
wx.scanCode,App 调 plus.barcode 或 RN 模块 - 开发者不直接接触 JSBridge,框架屏蔽了所有差异
这也是国内 Hybrid 的特殊情况:因为有微信小程序这个"超级 App"生态,跨端框架必须同时适配小程序,导致 JSBridge 反而被高度抽象化。
H5 离线包与 JSBridge 配合
H5 离线包 是 Hybrid 性能优化的杀手锏——把 H5 资源(HTML/JS/CSS/图片)预先打包下载到 App 本地,访问时完全不走网络,秒开率从 50% 提升到 95%+。
离线包的核心价值
| 指标 | 在线 H5 | 离线包 H5 |
|---|---|---|
| 首屏耗时 | 800ms~3s(受网络影响) | 100~300ms |
| 弱网/无网体验 | 白屏/失败 | 正常使用 |
| 服务端流量 | 每次访问消耗 | 仅首次下载 + 增量 |
| 灵活性 | 即改即生效 | 需推包 + 客户端检测 |
支付宝、淘宝、字节等超级 App 的 H5 页面 99% 都是离线包。
离线包架构
核心技术点
1. 资源拦截(关键)
WebView 加载页面时,原生层拦截每个资源请求,命中本地包就返回本地文件,否则走网络:
class OfflineWebViewClient : WebViewClient() {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
val url = request.url.toString()
// 1. 查询本地离线包映射表
val localFile = OfflinePackManager.getLocalFile(url)
if (localFile != null && localFile.exists()) {
// 2. 返回本地文件流
return WebResourceResponse(
getMimeType(url),
"UTF-8",
FileInputStream(localFile)
)
}
return super.shouldInterceptRequest(view, request) // 走网络
}
}
// 注册自定义 scheme(如 offlinepack://)
config.setURLSchemeHandler(OfflineSchemeHandler(), forURLScheme: "offlinepack")
class OfflineSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
guard let url = urlSchemeTask.request.url,
let localPath = OfflinePackManager.getLocalPath(for: url),
let data = try? Data(contentsOf: localPath) else {
urlSchemeTask.didFailWithError(...)
return
}
let response = URLResponse(url: url, mimeType: ..., ...)
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
}
2. 离线包格式(清单文件)
{
"appId": "promo-2026",
"version": "1.2.3",
"publishTime": "2026-04-29T10:00:00Z",
"signature": "sha256:abc...",
"files": [
{ "url": "/index.html", "hash": "...", "size": 1024 },
{ "url": "/static/main.js", "hash": "...", "size": 50000 }
],
"main": "/index.html"
}
3. 增量更新
通过 bsdiff 或 文件级 diff 生成补丁包,只下载差异部分。一个 5MB 的离线包,常规更新只需下载 50~200KB。
4. 与 JSBridge 的配合
// 检查当前包版本
const info = await bridge.call<{ appId: string; version: string }>('offlinePack.getInfo');
// 强制更新(用于运营紧急修复)
await bridge.call('offlinePack.checkUpdate', { force: true });
// 监听更新完成
bridge.on('offlinePack.updated', ({ appId, version }) => {
if (confirm(`发现新版本 ${version},是否刷新?`)) location.reload();
});
安全风险与防护
| 风险 | 防护 |
|---|---|
| 离线包被中间人篡改 | 签名校验(RSA/ECDSA)+ HTTPS 下载 |
解压时路径穿越(../../etc/passwd) | 校验解压路径必须在沙盒目录内 |
| 动态执行远程 JS 被 App Store 拒审 | iOS 仅做 Web 渲染,不动态加载二进制可执行文件 |
| 不同业务方离线包互相污染 | 按 appId 隔离沙盒目录、Cookie、Storage |
主流开源/商业方案
- 阿里支付宝 Nebula:业界最早的工业级方案(未完全开源)
- 腾讯 VasSonic:开源轻量方案,主打首屏 0 等待
- 字节跳动 Goofy/Forest:内部方案
- 京东 JDReact + 离线包
更详细的离线包设计可参考 设计 H5 海报生成系统 中的资源管理思路。
常见面试问题
Q1: JSBridge 的实现方式有几种?各自的优缺点?
答案:
主要有 4 种 JS → Native 通信方式:
| 方式 | 兼容性 | 性能 | 安全性 | 现状 |
|---|---|---|---|---|
| 注入 API(addJavascriptInterface / messageHandler) | Android 4.2+、iOS 8+ | 高 | 中(需白名单) | 主流 |
| 拦截 URL Scheme(iframe.src) | 极好 | 低(URL 长度限制) | 中 | 兼容老 WebView |
| 拦截 prompt/console | 较好 | 中 | 低 | 已淘汰 |
| WebSocket 长连接 | 好 | 中 | 高 | 特殊场景(远程调试) |
Native → JS 只有一种本质方式:evaluateJavascript 执行 JS 字符串。
Q2: Android addJavascriptInterface 的安全漏洞是什么?
答案:
漏洞背景:Android 4.2(API 17)以前,被注入的 Java 对象的所有 public 方法都会暴露给 JS。攻击者可通过反射调用 getClass()、forName() 等方法,任意执行 Java 代码(CVE-2012-6636)。
攻击示例:
// 老版本下,这段 JS 可以在设备上执行任意命令
NativeBridge.getClass().forName('java.lang.Runtime')
.getMethod('getRuntime').invoke(null)
.exec(['rm', '-rf', '/sdcard/']);
修复方案:
minSdkVersion >= 17,且方法必须加@JavascriptInterface注解才会暴露- 老版本可改用
WebViewClient.shouldOverrideUrlLoading拦截 URL Scheme - 严格 URL 白名单,禁止任意页面访问 Bridge
Q3: 如何处理 JSBridge 的异步回调?为什么需要 callbackId?
答案:
JS 调用 Native 是异步的(拍照、定位都需时间)。如果只用一个全局回调函数,多个并发调用的回调会互相覆盖。
解决方案:每次调用生成唯一 callbackId,存入 Map,Native 回调时携带该 ID 找到对应函数:
const callbacks = new Map<string, Function>();
let seed = 0;
function call(method: string, params: any, cb: Function) {
const callbackId = `cb_${++seed}`;
callbacks.set(callbackId, cb);
postMessage({ method, params, callbackId });
}
// Native 回调时
window.invokeCallback = (id, data) => {
callbacks.get(id)?.(data);
callbacks.delete(id); // 清理一次性回调
};
注意点:
- 一次性回调用完即删,否则内存泄漏
- 必须配合超时机制,否则 Native 不响应时 Promise 永久挂起
- 事件订阅(如
onPush)属于多次回调,需另外管理
Q4: iOS WKWebView 的 JSBridge 有什么坑?
答案:
- 内存泄漏:
userContentController.add(self, name:)强引用 self,导致 ViewController 无法释放。需用弱引用代理或deinit前主动remove。 - 不能传 Function:
postMessage只能传 JSON 可序列化的数据,函数会被丢弃。 - Cookie / Storage 隔离:WKWebView 与 NSURLSession 默认不共享 Cookie,Cookie 同步主要依赖
WKHTTPCookieStore;持久化数据还受WKWebsiteDataStore.default()/.nonPersistent()影响;多 WebView 之间还要统一WKProcessPool,否则登录态和存储行为容易不一致。 evaluateJavaScript必须主线程调用,否则 crash。
Q5: JSBridge 通信的性能瓶颈在哪里?如何优化?
答案:
瓶颈来源:
- JS ↔ Native 通信涉及线程切换(JS 线程 → Native 主线程)
- 数据需 JSON 序列化/反序列化
- URL Scheme 方案有 URL 长度限制(约 2KB),高频调用会丢消息
evaluateJavascript是异步的,频繁调用有调度开销
优化方案:
- 批量调用:合并多个小请求为一个大请求(类似 GraphQL 思想)
- 二进制传输:大数据用
ArrayBuffer,避免 base64 膨胀 - 本地缓存:稳定数据(设备信息、用户信息)首次拉取后 JS 端缓存
- 优先用注入 API 而非 URL Scheme
- 新架构:React Native 的 JSI 直接共享内存,零序列化开销
Q6: JSBridge 如何做版本兼容?
答案:
App 升级慢、用户分散在不同版本,新增 Bridge 方法可能在老版本上不存在。
处理方案:
// 1. 先获取 Native 支持的方法列表(首次启动缓存)
const supportedMethods = await bridge.call('getSupportedMethods');
// 2. 调用前检测能力
async function callSafely(method: string, params: any, fallback?: () => any) {
if (!supportedMethods.includes(method)) {
return fallback ? fallback() : Promise.reject('Not supported');
}
return bridge.call(method, params);
}
// 3. 使用语义化版本号
const { version } = await bridge.call('getAppInfo');
if (semver.gte(version, '5.2.0')) {
// 使用新 API
}
App 端策略:方法只增不删;废弃方法保留兼容层;API 设计预留可扩展字段。
Q7: JSBridge API 如何治理?
答案:
Bridge 一旦被多个业务长期使用,难点不只是“能不能调通”,而是 API 如何持续演进:
| 治理项 | 做法 |
|---|---|
| 命名规范 | 使用 domain.action,如 device.getInfo、ui.showToast、account.getToken |
| 权限分级 | 普通 API 默认开放,隐私/支付/账号 API 需要白名单、签名或用户确认 |
| 类型管理 | 从 Native 方法签名或统一 schema 生成 TypeScript 类型,避免手写文档漂移 |
| 版本演进 | 只增不删,废弃 API 保留兼容层;新增参数必须有默认值 |
| 监控告警 | 记录方法名、耗时、错误码、调用来源、App 版本,异常时能定位到具体页面 |
| 灰度开关 | 高风险 API 支持按 App 版本、用户分组、业务线开关控制 |
一个成熟的 JSBridge 往往会配套“API 注册中心”:统一维护方法说明、参数 schema、权限等级、最低 App 版本和负责人。这样 H5 侧可以做能力检测,Native 侧也能做自动化校验与审计。
Q8: JSBridge、Hybrid、WebView 三者的关系?
答案:
- WebView:操作系统提供的"浏览器内核控件"(Android
WebView、iOSWKWebView),是 H5 渲染容器 - Hybrid:一种架构模式——用 Native 容器嵌入 H5 页面,结合两者优势
- JSBridge:Hybrid 架构的通信层,连接 H5 与 Native
Hybrid App
├── Native 容器(导航、生命周期、性能监控)
├── WebView(H5 渲染)
└── JSBridge(双向通信)
典型场景:微信里的公众号文章、小程序的 web-view 组件、淘宝/京东 App 的活动页。
Q9: 设计一个 JSBridge 需要考虑哪些方面?
答案:
协议层:
- 统一消息格式(method/params/callbackId/code/data)
- 命名规范(如
device.getInfo、ui.showToast) - 错误码体系
功能层:
- Promise 化 API
- 回调管理(一次性回调 + 多次事件)
- 超时与重试
- 取消机制(AbortController)
兼容层:
- iOS/Android 平台差异屏蔽
- Bridge 加载顺序(H5 比 Bridge 先初始化怎么办?→ 队列暂存)
- 版本能力检测
安全层:
- URL/域名白名单
- 敏感方法签名
- HTTPS 强制 + SSL Pinning
工程化:
- TypeScript 类型自动生成(Native 方法签名 → TS 接口)
- Mock 调试(浏览器开发时 mock Native 响应)
- 调用日志、性能监控
Q10: H5 比 JSBridge 先初始化怎么办?
答案:
H5 加载完后,可能 Native 还未注入 window.NativeBridge,此时业务调用会失败。
解决方案:消息队列暂存:
class WebViewBridge {
private pendingQueue: BridgeRequest[] = [];
private isReady = false;
call(method: string, params: any) {
const req = { method, params, callbackId: genId() };
if (!this.isReady) {
this.pendingQueue.push(req); // 先入队
return new Promise(/* ... */);
}
this.send(req);
}
// Native 注入完成后调用
setReady() {
this.isReady = true;
this.pendingQueue.forEach((req) => this.send(req));
this.pendingQueue = [];
}
}
// Native 端在 WebView didFinishNavigation 后执行
webView.evaluateJavaScript("window.WebViewBridge.setReady()");
或更优雅的方式:Native 在文档加载前注入 Bridge(Android 用 WebViewClient.onPageStarted,iOS 用 WKUserScript + .atDocumentStart)。
Q11: JSBridge 与 RN/Flutter 的通信机制有什么区别?
答案:
| 维度 | 传统 JSBridge | React Native(旧 Bridge) | React Native(新 JSI) | Flutter(Platform Channel) |
|---|---|---|---|---|
| 通信方式 | 注入 API + evaluateJS | JSON 异步 Bridge | JSI 直接互调(同步可行) | MethodChannel(异步) |
| 序列化 | JSON | JSON | 无(共享内存) | 二进制(StandardCodec) |
| 同步调用 | 不支持 | 不支持 | 支持 | 不支持 |
| 性能 | 中 | 中(旧架构瓶颈) | 高 | 高 |
| 典型延迟 | 5-50ms | 5-50ms | < 1ms | 1-10ms |
核心区别:JSBridge 是字符串协议(JSON 序列化),新方案是二进制 / 内存共享,性能差距来源于此。
详见 React Native 基础与原理 和 Flutter 基础与原理。