1 引言

移动混合开发(Hybrid)已成为平衡多平台开发效率与原生体验的主流选择。WebView 承载 Web UI 的同时,需要访问摄像头、地理位置等原生能力,而 JavaScript 与 Native 代码分属不同运行时,直接互调不可行。JSBridge 正是解决这一问题的核心桥梁——它定义了一套可靠的通信协议,让 JavaScript 能够调用 Native 功能,也让 Native 能够主动通知 JavaScript 事件。

本文从零开始,讲解 JSBridge 的通信原理,并逐步实现一个支持双向调用的简易 Bridge,涵盖 Android 端集成与常见踩坑点。读完本文,你将掌握 JSBridge 的注入 API 实现方式,能够独立在项目中搭建或改进一套 Bridge 方案。

2 JSBridge 是什么?为什么需要它?

2.1 核心概念与出现背景

JSBridge 是一个运行在 WebView 环境中的通信中间件,它允许 JavaScript 调用原生代码的方法,也允许原生代码执行 JavaScript 函数。在混合开发中,Web 页面使用 HTML/CSS/JavaScript 构建 UI,具有快速迭代、跨平台复用的优势;但一些系统级功能(如相机、陀螺仪、文件选择)只能通过原生 API 才能访问。

通过 JSBridge,Web 页面可以像调用本地函数一样使用这些能力,同时原生层也可以在页面加载完毕或用户操作时主动回传数据。

JSBridge 的历史可追溯到桌面软件中用 Web UI 嵌入原生功能,在移动端则随着 Hybrid App 框架(如 Cordova、PhoneGap)流行而普及。如今不仅传统 Hybrid 方案依赖它,React Native、微信小程序等也采用类似桥接思路,只是通信底层由不同的运行时管理。理解 JSBridge 对掌握任何跨平台技术都有帮助。

2.2 通信原理基础

JSBridge 的通信模式主要分为两类:

  • JavaScript 调用 Native:有两种主流实现方式。一是注入 API,即 Native 端通过 WebView 的接口向 JavaScript 全局对象(window)注入一个原生对象,该对象的属性方法对应了原生功能,JS 调用它们时实际执行的是 Native 代码。

二是拦截 URL Scheme,JS 发起一个自定义协议的 URL 请求(如 jsbridge://openCamera?param=xx),Native 端在 WebView 的 shouldOverrideUrlLoading 回调中解析该 URL,匹配对应方法并执行。注入 API 方式性能更优、数据传递更灵活,是目前主流选择,本文采用此方案。

  • Native 调用 JavaScript:相比前者更简单。Native 端直接通过 WebView 提供的 API 执行一段 JavaScript 字符串。Android 中使用 webView.evaluateJavascript(script, callback),iOS 中使用 webView.evaluateJavaScript(script, completionHandler:)

这段 JS 代码可以是调用 Bridge 中预定义的函数,从而实现 Native 向 JS 发送消息。

两种方向组合起来,构成了一个双向通信闭环。关键在于消息协议的统一和回调处理。

3 动手实现:三步写出你自己的 JSBridge

以下步骤将指导你从零开始实现一个支持双向调用、带回调机制的 JSBridge。代码以 JavaScript 端与 Android Native 端为例,iOS 原理相似。

3.1 定义 Bridge 接口与消息格式

首先需要约定一套消息格式,让双方都能正确解析。推荐使用 JSON 对象,包含三个核心字段:

1
2
3
4
5
{
"bridgeName": "camera",
"data": { "quality": 0.8 },
"callbackId": "cb_1712345678"
}
  • bridgeName:要调用的原生功能名称,Native 端根据它分发到对应业务模块。
  • data:传递给原生功能的参数,JSON 格式,支持复杂结构。
  • callbackId:唯一标识一次调用回调的 ID,由 JS 端生成,Native 端在执行完逻辑后,会通过这个 ID 找到对应的回调函数并执行。支持异步回传场景。

为什么需要 callbackId?原生功能往往是异步的(如拍照、获取位置),JS 不能阻塞等待。因此需要一种机制:JS 先发一个请求,Native 完成后将结果附上原来的 callbackId 回传,JS 端根据 ID 取出存储的回调执行。

3.2 JavaScript 端(Web 侧)实现

以下是在浏览器或 WebView 中需要包含的 JavaScript 代码。它在 window 上挂载一个 JSBridge 对象,对外提供 callNativeonNativeCall 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* JSBridge - JavaScript 端实现
* 使用前需在页面加载完成后调用 JSBridge.init()
*/
;(function() {
// 存储等待回调的函数,key 为 callbackId
var callbackMap = {};
// 回调 ID 计数器
var callbackIdCounter = 0;

function generateCallbackId() {
return 'cb_' + (Date.now()) + '_' + (callbackIdCounter++);
}

window.JSBridge = {
/**
* 初始化:获取 Native 注入的对象,没有则创建占位
*/
init: function() {
// 约定 Native 注入的对象名为 NativeBridge
if (!window.NativeBridge) {
console.warn('JSBridge: NativeBridge not found, using mock');
window.NativeBridge = {
postMessage: function(jsonStr) {
// mock 实现,用于纯 Web 调试
console.log('mock callNative:', jsonStr);
// 模拟异步回调
setTimeout(function() {
var msg = JSON.parse(jsonStr);
var fakeResult = { message: 'mock result for ' + msg.bridgeName };
JSBridge.onNativeCall(JSON.stringify({
callbackId: msg.callbackId,
data: fakeResult,
bridgeName: msg.bridgeName + '_callback'
}));
}, 500);
}
};
}
},

/**
* JavaScript 调用原生功能
* @param {string} bridgeName - 功能名称
* @param {object} data - 参数
* @param {function} callback - 可选回调函数(原生完成后执行)
*/
callNative: function(bridgeName, data, callback) {
var callbackId = null;
if (typeof callback === 'function') {
callbackId = generateCallbackId();
callbackMap[callbackId] = callback;
}
var message = JSON.stringify({
bridgeName: bridgeName,
data: data || {},
callbackId: callbackId
});
// 通过 Native 注入的 postMessage 方法发送消息
window.NativeBridge.postMessage(message);
},

/**
* 原生调用 JavaScript 的入口
* @param {string} jsonStr - 原生传递过来的 JSON 字符串
*/
onNativeCall: function(jsonStr) {
var message;
try {
message = JSON.parse(jsonStr);
} catch (e) {
console.error('JSBridge: invalid JSON from native', jsonStr);
return;
}
// 如果是回调响应
if (message.callbackId && callbackMap[message.callbackId]) {
var cb = callbackMap[message.callbackId];
delete callbackMap[message.callbackId]; // 防止内存泄露
cb(message.data);
} else {
// 否则当作普通原生通知,可由业务方扩展处理
console.log('JSBridge: received native notify', message);
// 可触发自定义事件供页面监听
document.dispatchEvent(new CustomEvent('jsbridge_notify', { detail: message }));
}
}
};

// 自动初始化
window.JSBridge.init();
})();

关键点说明

  • NativeBridge.postMessage 是 Native 注入的 API,用于将 JSON 字符串传给 Native。
  • callbackMap 使用闭包管理回调,执行后立即删除,避免内存泄漏。
  • onNativeCall 是 Native 调用 JS 的入口,需在 Native 侧通过 evaluateJavascript 调用 window.JSBridge.onNativeCall(jsonString)

3.3 Native 端:Android 示例(WebView 注入 + 消息接收)

Android 端通过 @JavascriptInterface 注解暴露方法给 JavaScript。以下是一个完整的集成示例:

步骤 1:创建被注入的 Java 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class NativeBridge {
private WebView webView;
public NativeBridge(WebView webView) {
this.webView = webView;
}

@JavascriptInterface
public void postMessage(String jsonMessage) {
// 该方法会在 WebView 内核线程执行,不能直接操作 UI
// 可 post 到主线程处理
MessageHandler handler = new MessageHandler(webView);
handler.handleMessage(jsonMessage);
}
}

步骤 2:消息分发处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class MessageHandler {
private WebView webView;
public MessageHandler(WebView webView) { this.webView = webView; }

public void handleMessage(String jsonMessage) {
// 解析 JSON
try {
JSONObject msg = new JSONObject(jsonMessage);
String bridgeName = msg.getString("bridgeName");
JSONObject data = msg.optJSONObject("data");
String callbackId = msg.optString("callbackId");

// 根据 bridgeName 分发到具体功能模块
Object result = null;
switch (bridgeName) {
case "showDialog":
result = showDialog(data);
break;
case "getLocation":
result = getLocation(); // 异步示例,需用回调
break;
default:
result = "unknown bridge";
break;
}

// 如果有 callbackId,将结果回传 JS
if (callbackId != null && !callbackId.isEmpty()) {
JSONObject response = new JSONObject();
response.put("callbackId", callbackId);
response.put("data", result);
// 注意:evaluateJavascript 必须在主线程调用
webView.post(() -> {
webView.evaluateJavascript(
"javascript:JSBridge.onNativeCall('" + response.toString() + "')",
null
);
});
}
} catch (Exception e) {
e.printStackTrace();
}
}

private String showDialog(JSONObject data) {
// 模拟原生弹窗,实际应使用 AlertDialog
return "dialog shown";
}

private JSONObject getLocation() {
// 模拟位置获取,实际需调用 LocationManager
// 此处返回 mock 数据
JSONObject loc = new JSONObject();
try { loc.put("lat", 39.9); loc.put("lng", 116.3); } catch (Exception e) {}
return loc;
}
}

步骤 3:在 Activity 中配置 WebView

1
2
3
4
5
6
7
WebView webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
// 注入 NativeBridge 对象
NativeBridge bridge = new NativeBridge(webView);
webView.addJavascriptInterface(bridge, "NativeBridge");
// 加载页面
webView.loadUrl("file:///android_asset/index.html");

iOS 简要实现:使用 WKUserContentControlleraddScriptMessageHandler:name: 注册一个名为 NativeBridge 的消息处理器,Native 侧实现 userContentController:didReceiveScriptMessage: 方法接收 JS 发来的消息。

回传时使用 webView.evaluateJavaScript:completionHandler:。需要注意的是,WKUserContentController 中的 addScriptMessageHandler 会导致循环强引用,需在适当时机移除。

3.4 原生调用 JavaScript 的简单实现

Native 主动调 JS 的场景常见于:原生定位完成后通知页面、推送消息到达等。只需执行一段 JS 代码即可:

1
2
3
// Android
String js = "javascript:JSBridge.onNativeCall('" + jsonMessage + "')";
webView.evaluateJavascript(js, null);
1
2
3
// iOS (WKWebView)
NSString *js = [NSString stringWithFormat:@"JSBridge.onNativeCall('%@')", jsonMessage];
[webView evaluateJavaScript:js completionHandler:nil];

注意:jsonMessage 需要转义单引号或双引号,防止 JS 解析错误。推荐使用 JSON.stringify 方法在前端处理,Native 端将对象转为字符串后直接拼接。更安全的做法是 Native 端也构建一个 JSON 字符串,并在 JS 中再用 JSON.parse 解析,而不是直接拼接对象。

至此,一个简陋但功能完整的 JSBridge 就完成了。它支持同步/异步调用,双向通信,并且通过 callbackId 维护了回调关系。

4 用开源库快速集成:DSBridge 实践

手工实现 Bridge 虽然能帮助理解原理,但在生产环境中,直接用成熟的开源库可以节省大量时间并避免常见错误。DSBridge 是一个性能优秀、支持同步/异步和类型安全的开源 JSBridge 库(GitHub: wendux/DSBridge-Android / lzan13/DSBridge-iOS)。

4.1 DSBridge 简介与调度机制

DSBridge 的核心思想是:开发者只需在 Native 侧定义一个带 @DWebApi 注解的类,所有公开方法自动暴露给 JavaScript。JS 侧通过 dsBridge.call("apiName", args, callback) 调用,Native 方法返回值即为同步响应,若返回类型为 void 或需异步,则调用 handler.complete(data) 回传。

它自动处理了 callbackId 映射、线程切换和 JSON 序列化,大幅简化开发。

4.2 集成步骤(以 Android 为例)

  1. 添加依赖(build.gradle)
    1
    implementation 'com.github.wendux:DSBridge-Android:3.0-SNAPSHOT'
  2. 创建 API 类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class MyApi {
    @DWebApi
    public String getDeviceInfo(JSONObject args) {
    return Build.MODEL;
    }

    @DWebApi
    public void showToast(JSONObject args, Completer completer) {
    String msg = args.optString("msg");
    // 模拟异步操作
    new Handler(Looper.getMainLooper()).postDelayed(() -> {
    completer.complete("toast shown");
    }, 1000);
    }
    }
  3. 注册到 WebView
    1
    2
    3
    4
    5
    6
    WebView webView = findViewById(R.id.webView);
    webView.getSettings().setJavaScriptEnabled(true);
    // DSBridge 会接管消息传递
    webView.addJavascriptInterface(new MyApi(), "dsBridge");
    // 加载页面,页面中需要用 dsBridge.js 库
    webView.loadUrl("file:///android_asset/index.html");
  4. JS 侧调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script src="dsbridge.js"></script>
    <script>
    // 同步调用
    var device = dsBridge.call('getDeviceInfo', {});
    console.log(device);
    // 异步调用
    dsBridge.call('showToast', {msg: 'Hello'}, function(response) {
    console.log('callback:', response);
    });
    </script>

对比手工实现,DSBridge 的代码量减少了约 70%,且自动处理了线程问题(JS 到 Native 的调用默认在子线程执行,避免阻塞 UI)。对于大多数业务场景,推荐直接采用该类库,只在需要深度自定义或特殊安全策略时再考虑手写。

5 进阶技巧与典型踩坑记录

即使使用开源库,了解底层踩坑点依然有助于调试和优化。以下是在混合开发中常遇到的问题。

5.1 踩坑一:URL 长度限制与协议劫持

如果早期使用 URL Scheme 方式实现 JSBridge(而非注入 API),传递大数据(如 base64 编码的图片)会导致 URL 超过 WebView 的字符上限(通常约 2KB),从而被截断或完全失效。注入 API 方案可避免此问题,因为数据以 JSON 字符串形式直接传递,不受 URL 长度限制。

若必须使用 URL 方案,可将数据拆分为多个 fragment,通过连续请求拼接,或改用 postMessage 方式。实践中推荐直接采用注入 API,兼容性和性能都更好。

5.2 踩坑二:内存泄漏与线程安全

  • 回调未释放:在 JavaScript 端,如果 Native 长时间不回调(如用户取消定位),对应的 callback 会永远留在 callbackMap 中。解决方案:为每个 callbackId 设置超时定时器(如 10s),超时后自动移除并回调错误。如上节 JS 代码中未加入超时逻辑,生产环境需补充。

  • Native 端强引用:在 Android 中,addJavascriptInterface 注入的对象会被 WebView 内部的 JS 引擎持有强引用,若该对象又持有了 Activity 引用,会导致 Activity 无法被 GC 回收。常见场景:在 NativeBridge 类中持有 Activity contextWebView

建议注入对象只持有 WeakReference<WebView>,并在 Activity 销毁时手动解除注入(webView.removeJavascriptInterface("NativeBridge"))。

  • 线程问题@JavascriptInterface 方法运行在 WebView 内核线程,不能直接操作 UI。

需要 post 到主线程再执行。DSBridge 自动处理了这一转换,但手写时容易忽略。

5.3 进阶技巧:安全性与调试

  • 禁用危险配置:除必要的 setJavaScriptEnabled(true) 外,应禁用 setAllowFileAccess(true)setAllowContentAccess(true) 等权限,防止 XSS 攻击利用 Bridge 读取本地文件。对于敏感操作(如支付、隐私数据),应在 Native 端校验调用来源(如检查 referer 是否为合法域名)。

  • 调试通信日志:在开发阶段,启用 WebView 远程调试。Android 上调用 WebView.setWebContentsDebuggingEnabled(true) 后,可通过 Chrome DevTools 的 chrome://inspect 查看 WebView 的 console 输出。

在 JS 端 onNativeCallcallNative 中增加 console.log 打印消息详情,可快速定位通信失败原因。

  • 消息完整性校验:生产环境建议对消息内容做签名或校验,防止中间人攻击篡改 Bridge 消息。尤其是在金融类 App 中,需要验证 bridgeName 是否在白名单内。

6 总结与拓展

6.1 本文核心回顾

  • JSBridge 是混合开发中 JavaScript 与 Native 通信的桥梁,核心原理为注入 API执行 JavaScript 字符串

  • 手写 JSBridge 需要完成三件事:设计统一消息协议(bridgeName/data/callbackId)、JS 端维护回调队列、Native 端处理分发与结果回传。

  • 生产环境推荐使用 DSBridge 等成熟库,节省开发成本,但理解底层机制有助于排查复杂问题。

  • 常见踩坑点包括 URL 长度限制、内存泄漏(未释放回调或注入对象强引用)、线程安全和安全配置。

6.2 拓展学习方向

  • 与其他跨平台方案的桥接对比:React Native 使用 JavaScript 引擎(JSC/Hermes)作为中间层,通过 Native 模块注册和序列化桥接,消息是异步的;Flutter 则通过 Platform Channel 与原生通信,采用二进制序列化。深入对比这些方案的性能差异,能更精准地选择技术栈。

  • 封装 Bridge SDK:将本文的 JS 代码和 Native 端代码封装为 SDK 模块,提供给业务团队使用。抽象出注册 API 的接口,使业务方只需关注业务逻辑,无需关心通信细节。

  • 在小程序或 WKWebView 中的特殊限制:微信小程序的视图层与逻辑层分离,JSBridge 消息需通过 Native 转发;iOS WKWebView 由于进程间通信(IPC)限制,evaluateJavascript 的调用频率和大小均有上限,大数据传递需分片或改用其他方案。

通过以上实践,你已经掌握了 JSBridge 的核心实现与工程化要点。建议在项目中先评估需求复杂度:简单场景手写几行足够,复杂场景直接选用 DSBridge,并在集成后重点测试边界情况(大数据、快速点击、页面关闭等)。桥梁坚固,混合开发的道路才能走得更稳。