jsBridge 到底是什么?
jsBridge 是一种技术,主要用于解决 Web 前端和原生应用间的通信问题。这一技术在混合开发(Hybrid App Development)和一些原生应用内嵌 H5 页面的场景中被广泛应用。通过 jsBridge,开发者可以直接在 JavaScript 中调用原生代码,如获取设备信息、调用系统功能等,极大的提高了开发效率。
首先需要了解 WebView
WebView 是一个浏览器控件或者组件,它能够帮助开发者将网页或者 HTML 内容嵌入到原生应用中。
WebView 控件除了能加载指定的 URL 外,还可以对 URL 请求、JavaScript 的对话框、加载进度、页面交互进行强大的处理,之后会提到拦截请求、执行 js 脚本都依赖于此。
Android 中的 WebView:
Android 中的 WebView 是一个继承自 View 的控件,它可以加载并显示网页,同时也提供了一些方法供你与 JavaScript 交互。你可以使用 loadUrl(String url)
方法加载一个网页,使用 evaluateJavascript(String script, ValueCallback<String> resultCallback)
方法执行 JavaScript 代码。你还可以通过 addJavascriptInterface(Object object, String name)
方法向 JavaScript 环境中添加一个 Java 对象,使得 JavaScript 可以调用该对象的方法。
iOS 中的 WebView:
iOS 中提供了 UIWebView
和 WKWebView
两个用于展示网页的控件。UIWebView
在 iOS 2.0 就被引入,但自 iOS 8.0 起,Apple 推荐使用 WKWebView
替代 UIWebView
。 WKWebView
提供了 load(URLRequest)
和 loadHTMLString(String baseURL: URL?)
方法用于加载网页,通过 evaluateJavaScript(String completionHandler: ((Any?, Error?) -> Void)?)
方法执行 JavaScript 代码。同时,iOS 中的 WKWebView
通过 WKScriptMessageHandler
协议和 WKUserContentController
类来实现 Native 与 JavaScript 的交互。
了解 WebView 很重要,它才是连接原生和 Web 的桥梁。因为 Web 前端大多对原生开发不了解,如果一开始就去了解所谓的 jsBridge,反而会迷惑。
Web 和 Native 的交互
Web 和 Native 的交互分为 Native 调用 js 和 js 调用 Native。
Native -> js
原生调 js 的方式比较简单。JavaScript 作为解释性语言,最大的一个特性就是可以随时随地地通过解释器执行一段 js 代码,所以可以将拼接的 JavaScript 代码字符串,传入 js 解析器执行就可以,js 解析器在这里就是 WebView 组件。
所以 WebView 执行拼接的 JavaScript 字符串,从外部调用 JavaScript 方法,JavaScript 的方法必须在全局的 window 上。
Android
Android 4.4 之前只能用 loadUrl
来实现,效率低,无法获得返回结果,且调用的时候会刷新 WebView:
java
webView.loadUrl("javascript:" + javaScriptString);
Android 4.4 之后提供了 evaluateJavascript
来执行 js 代码,效率高,获取返回值方便,调用时候不刷新 WebView:
java
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value){
xxx
}
});
iOS
UIWebView 使用 stringByEvaluatingJavaScriptFromString
:
c
NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];
WKWebView 使用 evaluateJavaScript
:
c
[webView evaluateJavaScript:@"执行的JS代码" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
}];
swift
func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil)
// javaScriptString 需要调用的 JS 代码
// completionHandler 执行后的回调
js -> Native
简单的说,主要是两类方法:拦截 URL,注入 API。
拦截 URL Schema
URL Schema 是类 URL 的一种请求格式,格式如下:
<protocol>://<host>/<path>?<qeury>#fragment
我们可以自定义 jsBridge 通信的 URL Schema,比如:jsbridge://showToast?text=hello
Native 加载 WebView 之后,Web 发送的所有请求都会经过 WebView 组件,所以 Native 可以重写 WebView里的方法,从来拦截 Web 发起的请求,我们对请求的格式进行判断:
- 如果符合我们自定义的 URL Schema,对 URL 进行解析,拿到相关操作,进而调用原生 Native 的方法
- 如果不符合我们自定义的 URL Schema,我们直接转发,请求真正的服务
Web 发送 URL 请求的方法有这么几种:
a
标签location.href
- 使用
iframe.src
- 发送
ajax
请求
这些方法,a
标签需要用户操作,location.href
可能会引起页面的跳转丢失调用,发送 ajax
请求Android 没有相应的拦截方法,所以使用 iframe.src
是经常会使用的方案:
- 安卓提供了
shouldOverrideUrlLoading
方法拦截 - UIWebView 使用
shouldStartLoadWithRequest
,WKWebView 则使用decidePolicyForNavigationAction
Android:
java
public class CustomWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
......
// 场景一: 拦截请求、接收 scheme
if (url.equals("xxx")) {
// handle
...
// callback
view.loadUrl("javascript:setAllContent(" + json + ");")
return true;
}
return super.shouldOverrideUrlLoading(url);
}
}
iOS 的 WKWebview:
swift
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
if ([navigationAction.request.URL.absoluteString hasPrefix:@"xxx"]) {
[[UIApplication sharedApplication] openURL:navigationAction.request.URL];
}
decisionHandler(WKNavigationActionPolicyAllow);
}
这种方式有一定的缺陷:
- 使用 iframe.src 发送 URL SCHEME 会有 URL 长度的隐患。
- 创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。
注入 API
注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
iOS 的 UIWebView 提供了 JavaSciptCore
:
swift
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
// Native 逻辑
};
前端调用方式:
js
window.postBridgeMessage(message);
iOS的 WKWebView 提供了 WKScriptMessageHandler
:
swift
@interface WKWebVIewVC ()<WKScriptMessageHandler>
@implementation WKWebVIewVC
- (void)viewDidLoad {
[super viewDidLoad];
WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = [[WKUserContentController alloc] init];
WKUserContentController *userCC = configuration.userContentController;
// 注入对象,前端调用其方法时,Native 可以捕获到
[userCC addScriptMessageHandler:self name:@"nativeBridge"];
WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
// TODO 显示 WebView
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"nativeBridge"]) {
NSLog(@"前端传递的数据 %@: ",message.body);
// Native 逻辑
}
}
前端调用方式:
js
window.webkit.messageHandlers.nativeBridge.postMessage(message);
Android 提供了 addJavascriptInterface
:
java
public class JavaScriptInterface DemoActivity extends Activity {
private WebView Wv;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Wv = (WebView)findViewById(R.id.webView);
final JavaScriptInterface myJavaScriptInterface = new JavaScriptInterface(this);
Wv.getSettings().setJavaScriptEnabled(true);
Wv.addJavascriptInterface(myJavaScriptInterface, "nativeBridge");
// TODO 显示 WebView
}
public class JavaScriptInterface {
Context mContext;
JavaScriptInterface(Context c) {
mContext = c;
}
public void postMessage(String webMessage){
// Native 逻辑
}
}
}
前端调用方式:
js
window.nativeBridge.postMessage(message);
在 4.2 之前,Android 注入 JavaScript 对象的接口是 addJavascriptInterface
,但是这个接口有漏洞,可以被不法分子利用,危害用户的安全,因此在 4.2 中引入新的接口 @JavascriptInterface(上面代码中使用的)来替代这个接口,解决安全问题。所以 Android 注入对对象的方式是 有兼容性问题的。(4.2 之前很多方案都采用拦截 prompt 的方式来实现,因为篇幅有限,这里就不展开了。)
jsBridge 的实现(带回调的交互)
Native、Web 间可以交互,但站在一端而言还是一个单向通信的过程,比如站在 Web 的角度:Web 调用 Native 的方法,Native 直接进行相关操作但无法将结果返回给 Web,但实际使用中会经常需要将操作的结果返回,也就是 js 回调。
jsBridge 的接口主要功能有两个:调用 Native(给 Native 发消息) 和 被 Native 调用(接收 Native 消息) 。因此,jsBridge 可以设计如下:
js
window.JSBridge = {
// 调用 Native
invoke: function(msg) {
// 判断环境,获取不同的 nativeBridge
nativeBridge.postMessage(msg);
},
receiveMessage: function(msg) {
// 处理 msg
}
};
那么有回调的交互如何实现呢?
其实基于之前的单向通信就可以实现,我们在一端调用的时候在参数中加一个 callbackId
标记对应的回调,对端接收到调用请求后,进行实际操作,如果带有 callbackId
,对端再进行一次调用,将结果、callbackId
回传回来,这端根据 callbackId
匹配相应的回调,将结果传入执行就可以了。
可以看到实际上还是通过两次单项通信实现的。
js
(function () {
var id = 0,
callbacks = {};
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, callback, data) {
// 判断环境,获取不同的 nativeBridge
var thisId = id ++; // 获取唯一 id
callbacks[thisId] = callback; // 存储 Callback
window.nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {},
callbackId: thisId // 传到 Native 端
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {},
callbackId = msg.callbackId; // Native 将 callbackId 原封不动传回
// 具体逻辑
// bridgeName 和 callbackId 不会同时存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相应句柄
callbacks[callbackId](msg.data); // 执行调用
}
} elseif (bridgeName) {
}
}
};
})();
最后用同样的方式加上 Native 调用的回调逻辑,同时对代码进行一些优化,就大概实现了一个功能比较完整的 jsBridge。其代码如下:
js
(function () {
var id = 0,
callbacks = {},
registerFuncs = {};
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, callback, data) {
// 判断环境,获取不同的 nativeBridge
var thisId = id ++; // 获取唯一 id
callbacks[thisId] = callback; // 存储 Callback
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {},
callbackId: thisId // 传到 Native 端
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {},
callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
responstId = msg.responstId;
// 具体逻辑
// bridgeName 和 callbackId 不会同时存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相应句柄
callbacks[callbackId](msg.data); // 执行调用
}
} elseif (bridgeName) {
if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
var ret = {},
flag = false;
registerFuncs[bridgeName].forEach(function(callback) => {
callback(data, function(r) {
flag = true;
ret = Object.assign(ret, r);
});
});
if (flag) {
nativeBridge.postMessage({ // 回调 Native
responstId: responstId,
ret: ret
});
}
}
}
},
register: function(bridgeName, callback) {
if (!registerFuncs[bridgeName]) {
registerFuncs[bridgeName] = [];
}
registerFuncs[bridgeName].push(callback); // 存储回调
}
};
})();