介绍#
如今移动端盛行的年代,技术选型基本都是混合开发(Hybrid),即结合原生 Native 与 Web H5 两者的优点同时使用这两类技术:
- 原生技术指 iOS(Objective C、Swift)、Android(Java):开发效率较低,发布依赖用户更新,但性能更高且功能覆盖率更高
- Web 技术指 JavaScript:开发效率更高,发布更新灵活,但性能较低且功能特性也受限
这种模式下,H5 经常需要使用 Native 的功能,比如打开摄像头、查看本地相册、图片上传、页面分享等等,Native 也需要向 Web 端推送更新状态等。
而 JavaScript 是运行在单独的 JS Context(Webview 容器、JSCore 等),与原生运行环境有隔离,需要有一种机制实现 Native 和 Web 端的双向通信,这种机制就是 JSBridge:
以
JavaScript
引擎或Webview
容器作为媒介,通过协定协议进行通信,实现 Native 和 Web 双向通信的一种机制。
通过 JSBridge,Web 端可以调用 Native 端的 Java 接口,Native 端也可以调用 Web 端的 JS 接口,实现双向调用。
通信原理#
JS 调用 Native#
JavaScript 调用 Native 主要有三种方式:注入 API、拦截 URL SCHEME 和 重写 prompt 等 JS 全局方法
注入 API#
主要原理是:通过 Webview 提供的接口,向 JS 的运行环境(window)中注入对象或方法,让 JS 调用时直接执行相应的 Native 代码逻辑
iOS UIWebView 实现(主要是 JavaScriptCore
)
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
// Native 逻辑
};
前端调用
window.postBridgeMessage(message)
iOS WKWebView 实现(主要是 WKScriptMessageHandler
)
@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];
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"nativeBridge"]) {
NSLog(@"前端传递的数据 %@: ",message.body);
// Native 逻辑
}
}
前端调用
window.webkit.messageHandlers.nativeBridge.postMessage(message);
Android 实现
/**
在 4.2 之前,Android 注入 JavaScript 对象的接口是 addJavascriptInterface,
但是这个接口有漏洞,可以被不法分子利用,危害用户的安全,
因此在 4.2 中引入新的接口 @JavascriptInterface 来替代这个接口,解决安全问题。
所以 Android 注入对对象的方式是 有兼容性问题的
*/
publicclassJavaScriptInterfaceDemoActivityextendsActivity{
private WebView Wv;
@Override
publicvoidonCreate(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");
}
publicclassJavaScriptInterface{
Context mContext;
JavaScriptInterface(Context c) {
mContext = c;
}
publicvoidpostMessage(String webMessage){
// Native 逻辑
}
}
}
前端调用
window.nativeBridge.postMessage(message);
拦截 URL Scheme#
URL Scheme
是一种类似 url 的链接,为了方便 App 直接相互调用设计,格式如下:
<protocol>://<host>/<path>?<qeury>#fragment
自定义的一个 JSBridge 通信的 URL Scheme:jsbridge://showToast?text=hello
主要原理是:Native 加载 Webview 后,Web 发送的所有请求都会经过 Webview 组件,所以 Native 可以重写对应方法来拦截请求判断符合自定义的 URL Scheme 格式则进行解析处理进而调用原生 Native 方法
Web 发送 URL 请求有以下几种方式
a
标签(缺点:需要用户操作)location.href
(缺点:可能引起页面跳转丢失调用)ajax
请求(Android 没有相应拦截方法)- 使用
iframe src
以上方法中 iframe src
从早期开始就是经常使用的方案,兼容性很好:
- 安卓提供了
shouldOverrideUrlLoading
方法拦截 UIWebView
使用shouldStartLoadWithRequest
,WKWebView
则使用decidePolicyForNavigationAction
不过也有一定的缺陷:
- 基于 URL,长度有所限制
- 创建请求有一定耗时,比起注入 API 的方式调用同样功能耗时更长
重写 prompt 等 JS 全局方法#
Webview
有一个方法 setWebChromeClient
可以设置 WebChromeClient
对象,其中有三个方法 onJsAlert
、onJsConfirm
、onJsPrompt
,当 JS 调用 window.alert
、window.confirm
、window.prompt
时上面对应的方法也会被触发,利用这个机制也能做一些处理
由于拦截上述方法会对性能造成一定影响,因此需要选择使用频率较低的方法,而在 Android 中,相比其它几个方法,几乎不会使用到 prompt 方法,因此占用 prompt 是最佳方案。
Native 调用 JS#
Native 端调用 JS 端,这个比较简单,JavaScript 作为解释性语言,最大的一个特性就是可以随时随地地通过解释器执行一段 JS 代码,所以可以将拼接的 JavaScript 代码字符串,传入 JS 解析器执行就可以,JS 解析器在这里就是 Webview。
- iOS UIWebview 使用
stringByEvaluatingJavaScriptFromString
NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];
- iOS WKWebView 使用
evaluateJavaScript
[webView evaluateJavaScript:@"执行的JS代码" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
}];
- Android 4.4 之前只能使用
loadUrl
实现并且无法执行回调且调用的时候会刷新 WebView
String jsCode = String.format("window.showWebDialog('%s')", text);
webView.loadUrl("javascript: " + jsCode);
- Android 4.4 之后提供了
evaluateJavascript
来执行 JS 代码可以获取返回值执行回调
String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
}
});
带回调的调用#
上面提到的 Native、Web 双向通信的几种方法站在一端看还是一个单向通信的过程:Web 端调用 Native 方法,Native 直接进行相关操作但无法将结果返回 Web。
那么如何实现对端操作后返回结果呢?
基于之前的单向通信,一端调用时在参数中加一个 callbackId 标记对应的回调,对端收到调用请求后进行实际操作后再进行一次调用将结果、callbackId 回传回来,原端根据 callbackId 匹配相应的回调将结果传入执行即可,实际上两端都走了一次调用。
具体例子:在 Web 端点击按钮,获取 Native 端输入框的值,并将值以 Web 端弹窗展示。
// web 端代码
<body>
<div>
<button id="showBtn">获取 Native 输入,以 Web 弹窗展现</button>
</div>
</body>
<script>
let id = 1;
// 根据 id 保存callback
const callbackMap = {};
// 使用 JSSDK 封装调用与 Native 通信的事件,避免过多的污染全局环境
window.JSSDK = {
// 获取 Native 端输入框 value,带有回调
getNativeEditTextValue(callback) {
const callbackId = id++;
callbackMap[callbackId] = callback;
// 调用 JSB 方法,并将 callbackId 传入
window.NativeBridge.getNativeEditTextValue(callbackId);
},
// 接收 Native 端传来的 callbackId
receiveMessage(callbackId, value) {
if (callbackMap[callbackId]) {
// 根据 ID 匹配 callback,并执行
callbackMap[callbackId](value);
}
}
};
const showBtn = document.querySelector('#showBtn');
// 绑定按钮事件
showBtn.addEventListener('click', e => {
// 通过 JSSDK 调用,将回调函数传入
window.JSSDK.getNativeEditTextValue(value => window.alert('Natvie输入值:' + value));
});
</script>
// Android 端代码
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");
class NativeBridge {
private Context ctx;
NativeBridge(Context ctx) {
this.ctx = ctx;
}
// 获取 Native 端输入值
@JavascriptInterface
public void getNativeEditTextValue(int callbackId) {
MainActivity mainActivity = (MainActivity)ctx;
// 获取 Native 端输入框的 value
String value = mainActivity.editText.getText().toString();
// 需要注入在 Web 执行的 JS 代码
String jsCode = String.format("window.JSSDK.receiveMessage(%s, '%s')", callbackId, value);
// 在 UI 线程中执行
mainActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mainActivity.webView.evaluateJavascript(jsCode, null);
}
});
}
}
这样就实现了 Web -> Native 带有回调的调用,同理 Native -> Web 也是一样的逻辑(相对 callbackId 有 bridgeName 的概念)
这里也贴一张完整的调用时序图,可以看到整个过程跟 JSONP 的机制非常相像,需要两端遵循一套调用机制:
引用方式#
实际的 JSBridge 引用方式有以下两种,各有利弊:
方式 | 优点 | 缺点 |
---|---|---|
Native 端注入 | JSBridge 版本容易与 Native 对齐,不用考虑兼容 | 注入时机不确定,需要有失败重试机制 同时 JS 端调用时也要判断是否已注入 |
JS 端引用 | 时机可控,JS 直接调用方便 | 需要考虑 JSBridge 与 Native 之间多版本的兼容 |