跳到主要内容

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(混合应用)中连接 JavaScriptNative(iOS/Android) 的通信桥梁。它让运行在 WebView 中的 H5 页面能够调用 Native 能力(拍照、定位、扫码、支付、登录态等),同时也让 Native 能够主动通知 H5(页面生命周期、推送消息、网络变化等)。

JSBridge 的本质是 "约定一套跨语言的 RPC 协议":JS 把"调用方法名 + 参数 + 回调 ID"按约定格式发送给 Native,Native 执行后再通过约定的方式把结果回调给 JS。

核心要点
  • JS → Native 通信主要有 4 种方式:注入 API拦截 URL Scheme拦截 prompt/consoleMessageHandler
  • Native → JS 只有一种本质方式:执行 JS 代码evaluateJavascript / evaluateJavaScript
  • 现代主流方案:Android 用 addJavascriptInterface,iOS 用 WKScriptMessageHandler
  • 设计核心:统一协议 + 回调管理(callbackId) + 超时/错误处理 + 权限白名单

为什么需要 JSBridge?

H5 页面运行在 WebView 沙箱中,受同源策略、浏览器 API 能力和系统权限模型限制,很多设备能力无法稳定、完整地访问

能力浏览器 H5Hybrid + 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
// 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
// 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 平台兼容

webview-bridge.ts
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 端实现要点

Android JSBridge.kt
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)
}
}
}
Android 安全要点
  • @JavascriptInterface 注解是 Android 4.2+ 必需的安全防护,否则方法不会暴露给 JS
  • Android 4.2 以下的 addJavascriptInterface 存在 任意代码执行漏洞(CVE-2012-6636),因此 minSdkVersion >= 17 是行业底线
  • 必须做 URL 白名单校验,仅允许可信域名的页面调用敏感 API

iOS 端实现要点

iOS JSBridge.swift
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)
}
}
}
iOS 内存泄漏陷阱

WKUserContentController.add(self, name:)强引用 self,导致 WebView 和持有它的 ViewController 无法释放。

解决方案:使用一个弱引用代理对象转发消息,或在 ViewController deinit 前主动调用 removeScriptMessageHandler(forName:)


安全设计

JSBridge 是攻击面最大的前端组件之一,必须严格防护:

风险防护措施
任意页面调用敏感 API(拍照、支付)URL 白名单 + 域名校验,仅可信域名可调用
XSS 注入恶意 JS 调用 BridgeH5 端做 CSP;敏感操作要求二次确认
addJavascriptInterface RCE 漏洞Android 必须 minSdkVersion >= 17 + @JavascriptInterface
中间人攻击劫持 H5 页面仅允许 HTTPS、SSL Pinning
Bridge 方法被反编译枚举敏感方法加签名校验、设备绑定 Token
iOS WebView 缓存被恶意污染配置 WKWebsiteDataStore.nonPersistent()

权限分级与 URL 校验

生产环境不要只判断“域名是否在白名单”。一次 Bridge 调用至少应同时校验:

  • URL 完整性:校验 schemehost、端口和必要路径,避免把 https://trusted.com.evil.com 误判为可信域名
  • 能力分级:把 API 分为普通能力、隐私能力、交易/支付能力等等级,高风险能力需要登录态、签名、用户确认或服务端二次授权
  • 调用上下文:区分线上页面、离线包页面、内嵌第三方页面、调试页面,不同上下文暴露不同能力
  • 参数校验:Native 端必须重新校验参数类型、范围和业务权限,不能信任 H5 传入的数据

主流开源 JSBridge 方案对比

社区有几个经典的 JSBridge 开源实现,各有特点:

方案维护方协议方式平台支持同步调用适用场景
WebViewJavascriptBridgemarcuswestiniframe + 拦截 URL SchemeiOS / Android老项目兼容、UIWebView 时代
JsBridge (lzyzsd)lzyzsd拦截 URL Scheme(loadUrl)AndroidAndroid 早期项目
DSBridgewendux注入 API(Android @JavascriptInterface、iOS WKScriptMessageHandleriOS / Android✅(同步调用)现代项目首选,跨三端 API 统一
JSBridge (Hybrid SDK)各大厂自研注入 API + 协议层定制iOS / Android企业级超级 App(手淘、京东、字节)

DSBridge 的设计亮点

DSBridge 是目前 GitHub 上最成熟的开源 JSBridge 之一,几个关键设计值得借鉴:

  1. 同步 + 异步双模式:JS 端可声明同步调用 bridge.call('xxx', params) 直接拿返回值,也可异步 bridge.call('xxx', params, callback)
  2. 命名空间:方法名支持 namespace.method 形式,避免方法名冲突
  3. JS API 自动注册:原生端通过反射自动暴露所有 @JavascriptInterface 方法
  4. Native 主动调用 JS 也支持回调callHandler('jsMethod', args, returnValue -> {...})
DSBridge JS 端使用示例
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();
});
DSBridge 同步调用的代价

"同步调用"在 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 设计更现代化:

核心改进

维度CordovaCapacitor
通信底层URL Scheme 拦截 + addJavascriptInterfacepostMessage / messageHandlers
插件 API回调风格Promise / async-await
类型支持JS 为主TypeScript 优先,自带类型定义
Web 平台不支持支持(同一份代码可跑 Web、iOS、Android)
Native 工程隐藏在 CLI 后暴露原生工程(Xcode、Android Studio 直接打开)

典型用法

Capacitor JSBridge
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 加载页面时,原生层拦截每个资源请求,命中本地包就返回本地文件,否则走网络:

Android:WebViewClient 拦截
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) // 走网络
}
}
iOS:WKURLSchemeHandler
// 注册自定义 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. 离线包格式(清单文件)

manifest.json
{
"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 的配合

JS 端调用 Bridge 检查/触发更新
// 检查当前包版本
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/']);

修复方案

  1. minSdkVersion >= 17,且方法必须加 @JavascriptInterface 注解才会暴露
  2. 老版本可改用 WebViewClient.shouldOverrideUrlLoading 拦截 URL Scheme
  3. 严格 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 有什么坑?

答案

  1. 内存泄漏userContentController.add(self, name:) 强引用 self,导致 ViewController 无法释放。需用弱引用代理或 deinit 前主动 remove
  2. 不能传 FunctionpostMessage 只能传 JSON 可序列化的数据,函数会被丢弃。
  3. Cookie / Storage 隔离:WKWebView 与 NSURLSession 默认不共享 Cookie,Cookie 同步主要依赖 WKHTTPCookieStore;持久化数据还受 WKWebsiteDataStore.default() / .nonPersistent() 影响;多 WebView 之间还要统一 WKProcessPool,否则登录态和存储行为容易不一致。
  4. evaluateJavaScript 必须主线程调用,否则 crash。

Q5: JSBridge 通信的性能瓶颈在哪里?如何优化?

答案

瓶颈来源

  1. JS ↔ Native 通信涉及线程切换(JS 线程 → Native 主线程)
  2. 数据需 JSON 序列化/反序列化
  3. URL Scheme 方案有 URL 长度限制(约 2KB),高频调用会丢消息
  4. 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.getInfoui.showToastaccount.getToken
权限分级普通 API 默认开放,隐私/支付/账号 API 需要白名单、签名或用户确认
类型管理从 Native 方法签名或统一 schema 生成 TypeScript 类型,避免手写文档漂移
版本演进只增不删,废弃 API 保留兼容层;新增参数必须有默认值
监控告警记录方法名、耗时、错误码、调用来源、App 版本,异常时能定位到具体页面
灰度开关高风险 API 支持按 App 版本、用户分组、业务线开关控制

一个成熟的 JSBridge 往往会配套“API 注册中心”:统一维护方法说明、参数 schema、权限等级、最低 App 版本和负责人。这样 H5 侧可以做能力检测,Native 侧也能做自动化校验与审计。

Q8: JSBridge、Hybrid、WebView 三者的关系?

答案

  • WebView:操作系统提供的"浏览器内核控件"(Android WebView、iOS WKWebView),是 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.getInfoui.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 的通信机制有什么区别?

答案

维度传统 JSBridgeReact Native(旧 Bridge)React Native(新 JSI)Flutter(Platform Channel)
通信方式注入 API + evaluateJSJSON 异步 BridgeJSI 直接互调(同步可行)MethodChannel(异步)
序列化JSONJSON无(共享内存)二进制(StandardCodec)
同步调用不支持不支持支持不支持
性能中(旧架构瓶颈)
典型延迟5-50ms5-50ms< 1ms1-10ms

核心区别:JSBridge 是字符串协议(JSON 序列化),新方案是二进制 / 内存共享,性能差距来源于此。

详见 React Native 基础与原理Flutter 基础与原理


相关链接