JavaScript提供调用Native功能的接口。让混合开发中的前端部分可以方便地使用Native的功能(地址位置、打开摄像头,查看本地相册,指纹支付)
JSBridge是Native和非Native之间的桥梁,他的核心是构建Native和非Native间消息通信的通道(而且这个通信的通道是双向的)。
双向通信的通道:
- JS向Native发送消息:调用相关的功能、通知Native当前js的相关状态等
-
Native向JS发送消息。回溯调用结果,消息推送。通知js当前Native的状态等
H5与Native对比
JSbridge的实现原理
JavaScript是运行在一个单独的JS context中(例如:webView的Webkit引擎,JSCore),由于这些Context与原生运行环境的天然隔离,我们可以将这种情况与RPC(Remote Procedure Call,远程过程调用)通信进行类比。将Native与JavaScript的每次互相调用看做一次RPC调用
在JSBridge的设计中,可以把前端看做RPC的客户端。把Native端看做RPC的服务端。从而JSBridge要实现的主要逻辑就出现了;通信调用(Native与JS通信)和句柄解析调用。
JSbridge的双向通信原理
JS调用Native的方式
方式一:拦截URL Scheme
- Android:Webview提供了shouldOverrideUrlLoding方法来提供给Native拦截H5发送的URL Scheme请求
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可以根据拦截到的URL Scheme和对应的参数执行相关的操作
- (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);
}
- js调用
qunarhy://hy/url?url=ymfe.tech // 自定义的
protocol是qunarhy,host则是hy
window.webkit.messageHandlers.topicSelect.postMessage(`vdiscover://post_new_note?attach=${JSON.stringify(params)}`)
window.prompt(`vdiscover://post_new_note?attach=${encodeURIComponent(JSON.stringify(params))}`)
- URL SCHEME是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的url近似,主要区别是protocol和host一般是自定义的。
- 拦截URL SCHEME的主要流程是:Web端通过某种方式(例如:iframe.src)发送URL Scheme请求,之后Native拦截到请求并更具URL SCHEME(包括所带的参数)进行相关的操作
-
优点
-
不存在漏洞问题,使用灵活,可以实现H5和Native页面的无缝切换
- 场景:适用于快速迭代,快速开发上线,某一链接直接填写H5链接,在对应的Native页面开发完成前先跳转至H5页面。等Native页面开发完成之后再进行拦截,跳转至Native页面,此时H5链接不用修改
-
不存在漏洞问题,使用灵活,可以实现H5和Native页面的无缝切换
-
缺点:
-
当使用iframe.src来发送URL Scheme需要对URL长度作控制,使用复杂。速度较慢
- 有些方案为了避免url长度隐患的缺陷,在ios上采用了使用ajax发送同域请求的方式,并将参数放到head或body中,这样虽然避免了url长度的隐患,但WKWebView并不支持这种方式
- 为什么选择iframe.src不选择location.herf?(因为如果通过location.href连续调用Native,很容易丢失一些调用)
- 创建请求需要一定的耗时,比诸如API的方式调用同样的功能耗时更长。
-
当使用iframe.src来发送URL Scheme需要对URL长度作控制,使用复杂。速度较慢
方式二:注入API
基于WebView提供的能力,我们可以向Window上注入对象或方法,js可以直接使用window上的方法,这种方法js需要等到Native执行完对应的逻辑之后才能进行回调里的操作
- Android的WebView提供了addJavascriptInterface方法,支持Android4.2及以上的系统
gpcWebView.addJavascriptInterface(new JavaScriptInterface(), 'nativeApiBridge');
public class JavaScriptInterface {
Context mContext;
JavaScriptInterface(Context c) {
mContext = c;
}
public void share(String webMessage){
// Native 逻辑
}
}
- JS调用:
window.NativeApi.share(xxx);
- iOS的UIWebView提供了JavascriptScore方法,支持iOS7.0以上的系统,WKWebview提供window.webkit.messageHandlers方法,支持iOS8.0及以上系统。UIWebview在几年前常用,目前不常见
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 40.0;
configuration.preferences = preferences;
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"share"];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"pickImage"];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"share"];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"pickImage"];
}
- js调用示例
window.webkit.messageHandlers.share.postMessage(xxx);
方式三:重写prompt等原生js方法
- Android4.2之前注入对象的接口是addJavascriptInterface,但是由于一些安全的原因不被使用,一般会通过修改浏览器的部分window对象的方法来完成操作,主要是拦截alert、confirm、prompt、console.log四个方法。分别被Webview的onJsAlert、OnJsConfirm、onConsoleMessage、onJsPrompt监听
// onJsPrompt 监听
public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {
String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue);
xxx;
return true;
}
- iOS由于安全限制,WKWebView对alert、confirm、prompt等方法都做了拦截,如果通过此方式进行Native与js交互,需要实现WKWebView的三个WKUIDelegate代理方法
-(void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:([UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}])];
[self presentViewController:alertController animated:YES completion:nil];
}
使用这种方式时,可以与Android和iOS约定使用传参的格式,h5可以无需识别客户端,传入不同参数直接调用Native即可,剩下的交给客户端自己去拦截相同的方法,识别相同的参数,进行自己的处理逻辑即可实现多端表现一致。
js调用:
alert("确定xxx?", "取消", "确定", callback());
如果能与Native确定好方法名、传参等调用的协议规范,这样其他格式的prompt等方法是不会被识别的,能起到隔离的作用
Native调用JS的方式
Native调用js需要,JavaScript的方法必须在全局的window上即可
Android中主要有两种实现方法:
-
Android 4.4以前,通过loadUrl方法,执行一段js代码来实现
- loadUrl方式使用起来方便简洁,但是效率低无法获得返回结果且调用的时候会刷新WebView
-
Android 4.4以后可以使用evaluateJavascript方法
- evaluateJavascript方法效率高获取返回值方便。调用的时候不刷新WebView
webView.loadUrl("javascript:" + javaScriptString);
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value){
xxx
}
});
- ios在WKWebview中可以通过evaluateJavaScript:javaScriptString来实现,支持iOS8.0及以上系统
// swift
func evaluateJavaScript(_ javaScriptString: String,
completionHandler: ((Any?, Error?) -> Void)? = nil)
// javaScriptString 需要调用的 JS 代码
// completionHandler 执行后的回调
// objective-c
[jsContext evaluateJavaScript:@"ZcyJsBridge(ev, data)"]