一、背景
考虑到洞察报告后⾯会内嵌到原⽣App中,洞察报告的展⽰形式可以使⽤移动端H5的⽅式进⾏展⽰, 图表以及⽂本的配置需要做相关适配,具体图表如何适配移动端后⾯需要单独调研(需要先确定使⽤ 哪种图表)。⽽H5主要是运⾏在浏览器上,内嵌到App中,需要涉及到双⽅通信的交互,本⽂主要以 web端H5的⻆度调研并整理双⽅的交互及通信⽅式。
⼆、通信容器WebView
H5和原⽣App⼆者通信的桥梁是webView,webView是移动端(原⽣)提供的运⾏web的环境,它是⼀ 种嵌⼊式浏览器,原⽣应⽤可以⽤它来展⽰⽹络内容。可与⻚⾯JavaScript交互,实现混合开发。
iOS容器:UIWebView和WKWebView。
UIWebView是⼀个可加载⽹⻚的对象,它有浏览记录功能,且对加载的⽹⻚内容是可编程的。说⽩了 UIWebView有类似浏览器的功能,我们可以使⽤它来打开⻚⾯,并做⼀些定制化的功能,如可以让js 调某个⽅法可以取到⼿机的GPS信息。
注意⚠️:苹果发布iOS8的时候,新增了⼀个WKWebView组件容器,如果你的APP只考虑⽀持iOS8及 以上版本,那么你就可以使⽤这个新的浏览器控件了。 并且⽬前Apple要求新发布的app不能使⽤ UIWebView,现在应该⽤WKWebView。
Android容器: 在安卓客⼾端中,webView容器与⼿机⾃带的浏览器内核⼀致,多为androidchrome。不存在兼容性和性能问题。
RN容器 : 在react-native开发中,从rn 0.37版本开始官⽅引⼊了组件,在安卓中调⽤原⽣浏览器,在 IOS中默认调⽤的是UIWebView容器。从IOS12开始,苹果正式弃⽤UIWebView,统⼀采⽤ WKWebView。RN从0.57起,可指定使⽤WKWebView作为WebView的实现。
三、通信⽅式⸺介绍
1. JavaScript 调⽤ Native
JavaScript 调⽤ Native 的⽅式,主要有两种:注⼊ API 和 拦截 URL SCHEME。
注⼊API
注⼊ API ⽅式的主要原理是,通过 WebView 提供的接⼝,向 JavaScript 的 Context(window)中注 ⼊对象或者⽅法,让 JavaScript 调⽤时,直接执⾏相应的 Native 代码逻辑,达到 JavaScript 调⽤ Native 的⽬的。
拦截 URL SCHEME
URL SCHEME是⼀种类似于url的链接,是为了⽅便app直接互相调⽤设计的,形式和普通的 url 近 似,主要区别是 protocol 和 host ⼀般是⾃定义的,例如: qunarhy://hy/url?url=ymfe.tech, protocol 是 qunarhy,host 则是 hy。
拦截 URL SCHEME 的主要流程是:Web 端通过某种⽅式(例如 iframe.src)发送 URL Scheme 请 求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进⾏相关操作。
2. Native调⽤JavaScript
因为app是宿主,可以直接访问H5,所以这种调⽤⽐较简单,直接从外部调⽤ JavaScript 中的⽅法即 可,因此 Native调⽤JavaScript时,只需要将JavaScript 的⽅法挂载在全局的 window 上即可。
四、双向通信
1. H5和 iOS 的双向通信
iOS上UIWebView⽤JavaScriptCore框架,WKWebView⽤MessageHandler。
1.1 H5→IOS
因为H5不能直接访问宿主app,所以这种调⽤就⽐较复杂⼀点,常⽤的有两种⽅式:
⽅式⼀:拦截URL SCHEME
由 h5 发起⼀个⾃定义协议请求,app 拦截这个请求后,再由 app 调⽤ h5 中的回调函数。
在iOS中,并没有现成的 api 让 js 去调⽤ Native的⽅法,但是UIWebView与WKWebView能够拦截h5 内发起的所有⽹络请求,这些请求都可以通过delegate函数在iOS中得到通知,所以我们的思路就是通过在h5内发起约定好的特定协议的⽹络请求,即拦截URL SCHEME。
举个🌰:
javascript
// javascript
window.location.href = 'jsbridge://methodName?params=' + encodeURIComponent(obj)
请求地址: 'jsbridge://methodName?params=' + encodeURIComponent(obj)
,只需要在客⼾端WebView中发现是jsbridge://
开头的地址,就拦截该请求并解析约定的参数,就可以 使客⼾端执⾏相应的逻辑。
在H5中发起这种特定协议的请求⽅式分两种,都是通过拦截URL SCHEME的⽅式:
- 通过localtion.href; 缺点:通过location.href有个问题,就是如果我们连续多次修改 window.location.href的值,在Native层只能接收到最后⼀次请求,前⾯的请求都会被忽略 掉。创建请求,需要⼀定的耗时,功能复杂时,耗时会较⻓。
- 通过iframe的⽅式,即使⽤ iframe.src 发送 URL SCHEME
使⽤iframe⽅式,唤起Native客⼾端的分享组件为例:
ini
// h5 js code 将它封装一下
function createIframe(url){
var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
iframe.remove();
}, 100);
}
然后客⼾端通过拦截这个请求,并且解析出相应的⽅法和参数: 这⾥以ios为例:
swift
// IOS swift code
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
print("shouldStartLoadWithRequest")
let url = request.URL
let scheme = url?.scheme
let method = url?.host
let query = url?.query
if url != nil && scheme == "jsbridge" {
print("scheme == (scheme)")
print("method == (method)")
print("query == (query)")
switch method! {
case "getData":
self.getData()
case "putData":
self.putData()
case "gotoWebview":
self.gotoWebview()
case "gotoNative":
self.gotoNative()
case "doAction":
self.doAction()
case "configNative":
self.configNative()
default:
print("default")
}
return false
} else {
return true
}
}
拦截URL SCHEME总结
缺点: 使⽤ iframe.src 发送 URL SCHEME 会有 url ⻓度的隐患。有些⽅案为了规避 url ⻓度隐患 的缺陷,在 iOS 上采⽤了使⽤ Ajax 发送同域请求的⽅式,并将参数放到 head 或 body ⾥。这样,虽 然规避了 url ⻓度的隐患,但是 WKWebView 并不⽀持这样的⽅式。
优点: ⽀持 iOS6,但是⼤环境下iOS6 占⽐很⼩,这种⽅式并不优雅。
⽅式⼆:注⼊API
由 app 向 h5 注⼊⼀个全局 js 对象,然后在 h5 直接访问这个对象。 iOS 的 UIWebView实例如下:
ini
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
// Native 逻辑
};
在javascript中使⽤:
javascript
// javascript
window.postBridgeMessage(message);
对于 iOS 的 WKWebView 可以⽤以下⽅式:
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 逻辑
}
}
在javascript中使⽤:
ini
window.webkit.messageHandlers.nativeBridge.postMessage(message);
1.2 IOS→H5的通信方式
因为app是宿主,可以直接访问H5,所以这种调用比较简单,直接从外部调用 JavaScript 中的方法即可,因此JavaScript 的方法必须在全局的 window 上。那么native端调用JavaScript的方法如下:
通过UIWebView
组件的stringByEvaluatingJavaScriptFromString
或者WKWebView
的evaluateJavaScript
的方法来实现的,该方法返回js脚本的执行结果。
java
// IOS swift code
// UIWebView
webview.stringByEvaluatingJavaScriptFromString("window.methodName()")
// WKWebView
wkWebView evaluateJavaScript("window.methodName()")
使用这种方式调用js的方法,就需要将这个方法挂载到window下,从全局考虑,我们只要暴露一个对象如JSBridge让native调用就好了,所以在这里可以对Native的代码做一个简单的封装:
scss
//下面为伪代码
webview.setDataToJs(somedata);
webview.setDataToJs = function(data) {
webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}
2. H5和Android的双向通信
2.1 H5→Android
⽅式⼀:拦截URL SCHEME
通过WebViewClient
的 shouldOverrideUrlLoading
⽅法对url协议进⾏解析,这种js的调⽤ ⽅式与ios的⼀样。
⽅式⼆:注⼊原⽣js
通过在webview⻚⾯⾥直接注⼊原⽣js代码⽅式,使⽤ addJavascriptInterface
⽅法来实现, 这种⽅式最简洁,仅将Android对象和JS对象映射即可,但在 Android 4. 2 以下存在漏洞(点击查看具体文章)。因此在 4.2 中引⼊新的接⼝ @JavascriptInterface
来替代这个接⼝,解决安全问题。
在android里注入原生js代码实现如下:
typescript
class JSInterface {
@JavascriptInterface // 被JS调用的方法必须加入@JavascriptInterface注解
public String getUserData() {
return "UserData";
}
}
// 通过addJavascriptInterface()将Java对象映射到JS对象
//参数1:Javascript对象名
//参数2:Java对象名
webView.addJavascriptInterface(new JSInterface(), "AndroidJS"); //JSInterface类对象映射到js的AndroidJS对象
上面的代码就是在页面的window对象里注入了AndroidJS
对象。在js里可以直接调用
javascript
// js
window.AndroidJS.getUserData())
⽅式三:拦截js对话框信息
通过 WebChromeClient
的onJsAlert()
、onJsConfirm()
、onJsPrompt()
方法回调拦截JS对话框alert()
、confirm()
、prompt()
消息,一般我们使用prompt,因为它可以返回任意类型的值(alert()
对话框没有返回值;confirm()
对话框只能返回两种状态:确定 / 取消),并且这个在js里使用的不多,用来和native通讯副作用比较少。
- 通过
WebChromeClient
的onJsAlert()
、onJsConfirm()
、onJsPrompt()
方法回调拦截JS对话框alert()
、confirm()
、prompt()
消息,一般我们使用prompt,因为它可以返回任意类型的值(alert()
对话框没有返回值;confirm()
对话框只能返回两种状态:确定 / 取消),并且这个在js里使用的不多,用来和native通讯副作用比较少。
scss
//js
prompt("js://demo?arg1=111&arg2=222");
less
// Android
class SetWebChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 这里就可以对js的prompt进行处理,通过result返回结果
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
}
}
三种⽅式总结
调用方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
通过WebViewClient 的shouldOverrideUrlLoading 方法对url协议进行解析 |
不存在漏洞问题 | 使用复杂,需要约定协议,从native层向web层传递值比较繁琐; 会有 url 长度的隐患 | 不需要返回值的情况下的互调场景 |
通过在webview 页面里直接注入原生js代码方式,使用addJavascriptInterface 方法来实现 |
方便简洁 | Android 4.2以下版本存在漏洞 | Android 4.2以上版本相对简单的互调场景 |
通过 WebChromeClient 的onJsAlert() 、onJsConfirm() 、onJsPrompt() 方法回调拦截js对话框信息 |
不存在漏洞问题 | 使用复杂,需要进行协议的约束 | 能满足大多数情况下的互调场景,一般Android 4.2以下版本使用这种方式 |
2.2 Android→H5
与iOS类似,也是直接从外部调用 JavaScript 中的方法,将JavaScript 的方法必须在全局的 window 上。
Android调用window上的JS代码的方法有2种:
- 客户端通过
webview
的loadUrl
进行调用:
arduino
// android JAVA code
webView.loadUrl("javascript:window.jsBridge.getShare()");
H5端将方法绑定在window下的对象即可,无需与IOS作区分,需注意:JS代码调用一定要在 onPageFinished()
回调之后才能调用,否则调用不生效,onPageFinished()
属于WebViewClient
类的方法,主要在页面加载结束时调用。
- 通过
WebView
的evaluateJavascript()
typescript
webView.evaluateJavascript("javascript:window.jsBridge.getShare()",new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
}
});
二者区别:
loadUrl()
会刷新页面,evaluateJavascript()
则不会使页面刷新,所以evaluateJavascript()
的效率更高loadUrl()
得不到js
的返回值,对性能要求较低时可以使用这个方式;evaluateJavascript()
可以获取返回值evaluateJavascript()
在 Android 4.4 之后才可以使用
五、前端JSBridge的封装
1.什么是JSbridge
JSBridge主要是给 JavaScript 提供调用 Native 功能的接口,让混合开发中的前端部分可以方便地使用 Native 的功能(例如:地址位置、摄像头)。而且 JSBridge 的功能不止调用 Native 功能这么简单宽泛。实际上,JSBridge 就像其名称中的Bridge的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是构建 Native 和非 Native 间消息通信的通道,而且这个通信的通道是双向的。
2.JSBridge通信实现原理
上面的通信原理就是 JSBridge 实现的核心,实现方式可以各种各样,但是万变不离其宗。推荐的实现方式如下:
- JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)。
- Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。
3.JSBridge 接口实现
从上面的剖析中,可以得知,JSBridge 的接口主要功能有两个:调用 Native(给 Native 发消息) 和 接被 Native 调用(接收 Native 消息) 。因此,JSBridge 可以设计如下:
代码的实现过程可以直接看代码中的注释。
ini
(function () {
var id = 0,
callbacks = {},
registerFuncs = {};
window.JSBridge = {
// h5 调用 Native,调用时会将回调 id 存放到本地变量cbList中
invoke: function(bridgeName, callback, data) {
// 判断环境,获取不同的 nativeBridge
var thisId = id ++; // 获取唯一 id
callbacks[thisId] = callback; // 存储 Callback
// 调用native将参数传递进去 ==> 通信方式可任意选择
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
});
}
}
}
},
// js注册方法,native主动发起调用
register: function(bridgeName, callback) {
if (!registerFuncs[bridgeName]) {
registerFuncs[bridgeName] = [];
}
registerFuncs[bridgeName].push(callback); // 存储回调
}
};
}
)();
调用示例
javascript
// 主动发消息
JSBridge.invoke('ui.callNative', (data) => data,{})
// 注册在本地,被动接受客户端调用
JSBridge.register("ui.datatabupdate", (data) => data);
4. JSBridge如何引⽤
对于 JSBridge 的引⽤,常⽤有两种⽅式,各有利弊。
4.1由Native端进⾏注⼊。
注⼊⽅式和 Native 调⽤ JavaScript 类似,直接执⾏桥的全部代码。
优点:桥的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容。
缺点:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先判断 JSBridge 是否已经注入成功。
4.2由JavaScript端引⽤
直接与 JavaScript ⼀起执⾏。与由 Native 端注⼊正好相反。 优点:JavaScript 端可以确定 JSBridge 的存在,直接调用即可。
缺点:如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。
六、补充
⽀持跨平台的JSbridge⸺DSBridge