JSBridge实现原理

JSBridge是什么

JSBridge本质就是:Web端和客户端Native之间的通信桥梁,让混合开发模式中的Web端和Native端能够互相通信,实现双向调用。

Web端调用Native端

在Webview中注入JS API

通过WebView提供的接口,App将Native的相关接口注入到JS的Context(window)的对象中
Web端就可以直接在全局 window 下使用这个暴露的全局JS对象,进而调用原生端的方法

Android注入方法:

  • addJavascriptInterface配合@JavascriptInterface:Android 4.2+ 配合 @JavascriptInterface 才安全可用

IOS注入方法:

  • WKWebView + WKScriptMessageHandler 是 iOS 官方方案,推荐新项目优先使用,链路更清晰。
  • WebViewJavascriptBridge 是常见历史封装方案,接入快、兼容旧项目,但初始化握手和调试成本更高。

示例代码:

IOS走WebViewJavascriptBridge方案、Android走addJavascriptInterface方案

1
2
3
4
5
6
7
8
9
10
11
12
// Android 侧:通过 addJavascriptInterface 注入 JSAPI
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");

class NativeBridge {
private Context ctx;
NativeBridge(Context ctx) { this.ctx = ctx; }

@JavascriptInterface
public void showNativeDialog(String text) {
new AlertDialog.Builder(ctx).setMessage(text).create().show();
}
}
1
2
3
4
5
6
// iOS 侧(示意):WebViewJavascriptBridge 注册 handler
bridge.registerHandler("showNativeDialog") { data, responseCallback in
let text = (data as? [String: Any])?["text"] as? String ?? ""
// 展示原生弹窗...
responseCallback?(["code": 0, "msg": "ok"])
}
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
// H5 侧:统一入口,按环境分发调用
function isAndroidBridge() {
return !!window.NativeBridge && typeof window.NativeBridge.showNativeDialog === "function";
}

function isiOSBridgeReady() {
return !!window.WebViewJavascriptBridge && typeof window.WebViewJavascriptBridge.callHandler === "function";
}

// IOS适配比较复杂,因为iOS里bridge注入是握手注入,H5触发__bridge_loaded__才注入桥
function setupIOSBridge(callback) {
// 1) 桥已经注入完成:直接拿 bridge 调用
if (isiOSBridgeReady()) return callback(window.WebViewJavascriptBridge);
// 2) 桥还没就绪,但初始化流程已经开始:把当前回调先缓存起来
if (window.WVJBCallbacks) return window.WVJBCallbacks.push(callback);
// 3) 首次初始化:创建回调队列,并触发一次“桥初始化信号”
window.WVJBCallbacks = [callback];
const iframe = document.createElement("iframe");
iframe.style.display = "none";
// 通知 Native 注入 WebViewJavascriptBridge
iframe.src = "https://__bridge_loaded__";
document.documentElement.appendChild(iframe);
// 信号发出后移除 iframe,避免污染 DOM
setTimeout(() => document.documentElement.removeChild(iframe), 0);
}

function showDialog(text) {
if (isAndroidBridge()) {
// Android: addJavascriptInterface 注入到 window.NativeBridge
window.NativeBridge.showNativeDialog(text);
return;
}

setupIOSBridge((bridge) => {
// iOS: WebViewJavascriptBridge
bridge.callHandler("showNativeDialog", { text }, function (res) {
console.log("ios callback:", res);
});
});
}

showDialog("hello from h5");

URL Schema

URL Schema是类URL的一种请求格式,格式如下:

1
2
3
4
<protocol>://<host>/<path>?<qeury>#fragment

// 我们可以自定义JSBridge通信的URL Schema,比如:
hellobike://showToast?text=hello

Native加载WebView之后,Web发送的所有请求都会经过WebView组件,所以Native可以重写WebView里的方法,从来拦截Web发起的请求,我们对请求的格式进行判断:

符合我们自定义的URL Schema,对URL进行解析,拿到相关操作、操作,进而调用原生Native的方法
不符合我们自定义的URL Schema,我们直接转发,请求真正的服务

例如

1
2
3
4
5
6
7
8
9
10
get existOrderRedirect() {
let url: string;
if (this.env.isHelloBikeApp) {
url = 'hellobike://hellobike.com/xxxxx_xxx?from_type=xxxx&selected_tab=xxxxx';
} else if (this.env.isSFCApp) {
url = 'hellohitch://hellohitch.com/xxx/xxxx?bottomTab=xxxx';
}
return url;
}

URL Schema 这套桥接本质就是:

  • Web 侧构造一个特定 URL(如 myapp://share?…)
  • 通过 location.href / iframe.src / a 标签触发一次“导航请求”
  • Native 侧在 WebView 的导航拦截回调里识别这个 URL
  • 命中自定义 schema 就不真的跳转页面,而是解析参数并调用对应 Native 能力
  • 未命中就放行,正常加载网页链接

这种方式从早期就存在,兼容性很好,但是由于是基于URL的方式,长度受到限制而且不太直观,同时数据格式有限制,而且建立请求有时间耗时。

Native端调用Web端

Native端调用Web端,本质上其实是Natvie有运行JS的能力(WebView),所以Web端把方法挂在Window上,Native就能够执行这段JS代码

Android

Android提供了evaluateJavascript来执行JS代码,并且可以获取返回值执行回调:

1
2
3
4
5
6
7
String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {

}
});

IOS

IOS的WKWebView使用evaluateJavaScript

1
2
3
4
[webView evaluateJavaScript:@"执行的JS代码"
completionHandler:^(id _Nullable response, NSError * _Nullable error) {
//
}];

一种JSBridge的实现方式

Web侧挂全局函数:Web端在每次调用Native方法后在window上挂返回结果的回调,Native端把调用方法的结果通过返回结果的回调传回给Web端

可能有点绕,直接看下代码:

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
public class MainActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.webview_layout);
webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
// window 上添加 JsBridge 对象
webView.addJavascriptInterface(new JsBridge(), "JsBridge");
webView.loadUrl("http://10.168.2.149:5500/h5-test.html");
}

// 通过 addJavascriptInterface 方法在 window 上添加 JsBridge 对象
public class JsBridge {
// 添加 sendDataToApp 到 window.JsBridge 上
@JavascriptInterface
public void sendDataToApp(String value) {
// 获取数据
sendResponseToH5(value);
}
// 把返回结果返回给Web侧
public void sendResponseToH5(final String data) {
runOnUiThread(() -> {
webView.evaluateJavascript("javascript:window.JsBridge.receiveDataFromApp('" + data + "')", null);
});
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sendDataToApp() {
if (window.JsBridge && typeof window.JsBridge.sendDataToApp === "function") {
// Web侧把返回结果的方法挂在Window上,Native侧通过该方法返回结果
window.JsBridge.receiveDataFromApp = function (data) {
document.getElementById("app").innerHTML =
"Received data from App: " + data;
delete window.window.JsBridge.receiveDataFromApp;
};

// Web侧调用Native方法
window.JsBridge.sendDataToApp("Hello from Web");
document.getElementById("log").innerHTML = "Data sent to App";
} else {
document.getElementById("log").innerHTML = "sent data failed";
}
}

这种实现方式缺点其实很明显:

  • 全局污染:挂了一堆临时全局函数。
  • 不支持并发调用:后面挂的全局函数会覆盖前面的

另一种实现方式:事件派发

Native在返回结果时不直接调某个全局函数,而是通过事件派发的方式

这种方式的核心是:

  • Web 调 Native 前先生成一个requestId,吊用Native方法时带上这个requestId
  • Native 返回时统一触发同一个事件(例如 native:callback),并传回requestId
  • Web 通过 requestId 判断这条回包是不是自己的

直接看示例:

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
public class MainActivity extends AppCompatActivity {
private WebView webView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.webview_layout);
webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JsBridge(), "JsBridge");
webView.loadUrl("http://10.168.2.149:5500/h5-test.html");
}

public class JsBridge {
@JavascriptInterface
public void sendDataToApp(String value) {
// 假设 value 为 {"requestId":"xxx","payload":"Hello from Web"}
sendResponseToH5(value);
}

private void sendResponseToH5(final String data) {
runOnUiThread(() -> {
// Native 通过 dispatchEvent 触发统一事件通道
String jsCode = "window.dispatchEvent(new CustomEvent('native:callback', { detail: " + data + " }))";
webView.evaluateJavascript(jsCode, null);
});
}
}
}
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
function sendDataToAppByEventBus() {
if (window.JsBridge && typeof window.JsBridge.sendDataToApp === "function") {
const requestId = `req_${Date.now()}_${Math.random().toString(16).slice(2)}`;
const payload = {
requestId,
payload: "Hello from Web",
};

const callback = (event) => {
if (!(event instanceof CustomEvent)) return;
const detail = event.detail || {};

// 只处理当前请求对应的回包
if (detail.requestId !== requestId) return;

document.getElementById("app").innerHTML =
"Received data from App: " + JSON.stringify(detail);

// 处理完移除监听,避免泄漏
window.removeEventListener("native:callback", callback);
};

// 监听统一事件通道
window.addEventListener("native:callback", callback);

// Web 调 Native
window.JsBridge.sendDataToApp(JSON.stringify(payload));
document.getElementById("log").innerHTML = "Data sent to App by event bus";
} else {
document.getElementById("log").innerHTML = "sent data failed";
}
}

这种方式相比“全局函数回调”更适合并发场景:

  • 可以同时监听多次调用,不会出现 window.xxxResult 被覆盖的问题
  • 通过 requestId 可以准确匹配每次调用的返回结果