+ (void)webViewRunJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
defaultText:(NSString *)defaultText
completionHandler:(void (^)(NSString * _Nullable))completionHandler
{
/** Triggered by JS:
var person = prompt("Please enter your name", "Harry Potter");
if (person == null || person == "") {
txt = "User cancelled the prompt.";
} else {
txt = "Hello " + person + "! How are you today?";
}
*/
if (xxx) {
BLOCK_EXEC(completionHandler, text);
} else {
BLOCK_EXEC(completionHandler, nil);
}
}
在安卓4.4以上的版本一般使用 `evaluateJavascript` 这个 API 来调用。这里需要判断一下版本。
if (Build.VERSION.SDK_INT > 19) //see what wrapper we have
{
webView.evaluateJavascript("javascript:foo()", null);
} else {
webView.loadUrl("javascript:foo()");
}
前言
上一篇介绍了移动端开发的相关技术,这一篇主要是从 Hybrid 开发的 JS Bridge 通信讲起。
顾名思义,JS Bridge 的意思就是桥,这是一个连接 JS 和 Native 的桥接,也是 Hybrid App 里面的核心。一般分为 JS 调用 Native 和 Native 主动调用 JS 两种形式。
URL Scheme
URL Scheme 是一种特殊的 URL,一般用于在 Web 端唤醒 App,甚至跳转到 App 的某个页面,比如在某个手机网站上付款的时候,可以直接拉起支付宝支付页面。
这里有个小例子,你可以在浏览器里面直接输入
weixin://
,系统就会提示你是否要打开微信。输入mqq://
就会帮你唤起手机 QQ。这里有个常用 App URL Scheme 汇总:URL Schemes 收集整理
在手机里面打开这个页面后点击这里,就会提示你是否要打开微信。
我们常说的 Deeplink 一般也是基于 URL Scheme 来实现的。一个 URI 的组成结构如下:
除了 http/https 这两个常见的协议,还可以自定义协议。借用维基百科的一张图:
通常情况下,App 安装后会在手机系统上注册一个 Scheme,比如
weixin://
这种,所以我们在手机浏览器里面访问这个 scheme 地址,系统就会唤起我们的 App。一般在 Android 里面需要到 AndroidManifest.xml 文件中去注册 Scheme:
在 iOS 中需要在 Xcode 里面注册,有一些已经是系统使用的不应该使用,比如 Maps、YouTube、Music。具体可以参考苹果开发者官网文档:Defining a Custom URL Scheme for Your App
JS 调用 Native
在 iOS 里面又需要区分 UIWebView 和 WKWebView 两种 WebView:
WKWebView 是 iOS8 之后出现的,目的是取代笨重的 UIWebView,它占用内存更少,大概是 UIWebView 的 1/3,支持更好的 HTML5 特性,性能更加强大。 但也有一些缺点,比如不支持缓存,需要自己注入 Cookie,发送 POST 请求的时候带不了参数,拦截 POST 请求的时候无法解析参数等等。
JS 调用 Native 通信大致有三种方法:
这三种方式总体上各有利弊,下面会一一介绍。
拦截 Scheme
仔细思考一下,如果是 JS 和 Java 之间传递数据,我们该怎么做呢? 对于前端开发来说,调 Ajax 请求接口是最常见的需求了。不管对方是 Java 还是 Python,我们都可以通过 http/https 接口来获取数据。实际上这个流程和 JSONP 更加类似。
已知客户端是可以拦截请求的,那么可不可以在这个上面做文章呢?
如果我们请求一个不存在的地址,上面带了一些参数,通过参数告诉客户端我们需要调用的功能呢?
比如我要调用扫码功能:
客户端可以拦截这个请求,去解析参数上面的
func
来判断当前需要调起哪个功能。客户端调起扫码功能之后,会获取 WebView 上面的 callbacks 对象,根据 callback_id 回调它。所以基于上面的例子,我们可以把域名和路径当做通信标识,参数里面的 func 当做指令,callback_id 当做回调函数,其他参数当做数据传递。对于不满足条件的 http 请求不应该拦截。
当然了,现在主流的方式是前面我们看到的自定义 Scheme 协议,以这个为通信标识,域名和路径当做指令。
这种方式的好处就是 iOS6 以前只支持这种方式,兼容性比较好。
JS 侧
我们有很多种方法可以发起请求,目前使用最广泛的是 iframe 跳转:
iframe 跳转
Android 侧
在 Android 侧可以用
shouldOverrideUrlLoading
来拦截 url 请求。iOS 侧
在 iOS 侧需要区分 UIWebView 和 WKWebView 两种方式。 在 UIWebView 中:
在 WKWebView 中:
目前不建议只使用拦截 URL Scheme 解析参数的形式,主要存在几个问题。
location.href
会出现消息丢失,因为 WebView 限制了连续跳转,会过滤掉后续的请求。弹窗拦截
Android 实现
这种方式是利用弹窗会触发 WebView 相应事件来拦截的。一般是在
setWebChromeClient
里面的onJsAlert
、onJsConfirm
、onJsPrompt
方法拦截并解析他们传来的消息。iOS 实现
我们以 WKWebView 为例:
// 获取 JS 上下文 JSContext context = [webview valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; // 注入 Block context[@"callHandler"] = ^(JSValue data) { // 处理调用方法和参数 // 调用 Native 功能 // 回调 JS Callback }
window.callHandler({ type: "scan", data: "", callback: function(data) { } });
WKWebView wkWebView = [[WKWebView alloc] init]; WKWebViewConfiguration configuration = wkWebView.configuration; WKUserContentController *userCC = configuration.userContentController;
// 注入对象 [userCC addScriptMessageHandler:self name:@"nativeObj"]; // 清除对象 [userCC removeScriptMessageHandler:self name:@"nativeObj"];
// 客户端处理前端调用
window.webkit.messageHandlers.nativeObj.postMessage(data);
public void addJavascriptInterface() { mWebView.addJavascriptInterface(new DatePickerJSBridge(), "DatePickerBridge"); } private class PickerJSBridge { public void _pick(...) { } }
window.DatePickerBridge._pick(...)
private class PickerJSBridge { @JavascriptInterface public void _pick(...) { } }
webView.loadUrl("javascript:foo()")
if (Build.VERSION.SDK_INT > 19) //see what wrapper we have { webView.evaluateJavascript("javascript:foo()", null); } else { webView.loadUrl("javascript:foo()"); }
results = [self.webView stringByEvaluatingJavaScriptFromString:"foo()"];
[self.webView evaluateJavaScript:@"document.body.offsetHeight;" completionHandler:^(id _Nullable response, NSError * _Nullable error) { // 获取返回值 response }];
那么这几个 API 又是如何实现的呢?这里 Android 和 iOS 封装不一致,应当分开来说。
Android Bridge
前面我们有说过安卓可以通过
@JavascriptInterface
注解来将对象和方法暴露给 JS。所以这里的几个方法都是通过注解暴露给 JS 来调用的,在 JS 层面做了一些兼容处理。hasHandler
首先最简单的是这个
hasHandler
,就是在客户端里面维护一张表(其实我们是写死的),里面有支持的 Bridge 模块信息,只需要用switch...case
判断一下就行了。callHandler
然后我们来看
callHandler
这个方法,它是提供 JS 调用 Native 功能的方法。在调用这个方法之前,我们一般需要先判断一下 Native 是否支持这个功能。如果 Native 没有支持这个 Bridge,我们就需要对回调进行兼容性处理。这个兼容性处理包括两个方面,一个是功能方面,一个是 callback 的默认回参。
比如我们调用 Native 的弹窗功能,如果客户端没支持这个 Bridge,或者我们是在浏览器里面打开的这个页面,此时应该退出到使用 Web 的
alert
弹窗。对于 callback,我们可以默认给传个 0,表示当前不支持这个功能。假设这个
alert
的 bridge 接收两个参数,分别是title
和content
,那么此时就应该使用浏览器自带的alert
展示出来。这个
fallback
函数我们希望能够更加通用,每个调用方法都应该有自己的fallback
函数,所以前面的callHandler
应该设计成这样:我们可以基于这个函数封装一些功能方法,比如前面的 alert:
具体效果类似下面这种,这是从 Google 上随便找的一张图(侵删):
那么客户端又如何实现回调 callback 函数的呢?前面说过,客户端想调用 JS 方法,只能调用挂载到
window
对象上面的。因此,这里使用了一种很巧妙的方法,实际上 callback 函数依然是 JS 执行的。在调用 Native 之前,我们可以先将 callback 函数和一个 uniqueId 映射起来,然后存在 JS 本地。我们只需要将 callbackId 传给 Native 就行了。
在客户端这里,当 send 方法接收到参数之后,会执行相应功能,然后使用
webView.loadUrl
主动调用前端的一个接收函数。所以 JS 需要事前定义好这个
onReceive
方法,它接收一个 callbackId 和一个 result。大致流程如下:
registerHandler
注册的流程比较简单,也是我们把 callback 函数事先存到一个
messageHandler
对象里面,不过这次的 key 不再是一个随机的 id,而是name
。这里不像
callHandler
需要主动调用window.bridge.send
去通知客户端,只需要等客户端到了相应的时机来调用window.bridge.onReceive
就行了。 所以这里还需要改造一下onReceive
方法。由于不再会有 callbackId 了,所以客户端可以传个空值,然后将handlerName
放到 result 里面。这种情况下的流程如下,可以发现完全不需要 JS 调用 Native:
iOS Bridge
讲完了 Android,我们再来讲讲 iOS,原本 iOS 可以和 Android 设计一致,可是由于种种原因导致有不少差异。
iOS 和 Android 中最显著的差异就在于这个
window.bridge.send
方法的实现,Android 里面是直接调用 Native 的方法,iOS 中是通过 URL Scheme 的形式调用。协议依然是 WebViewJavaScriptBridge 里面的协议,URL Scheme 本身不会传递数据,只是告诉 Native 有新的调用。
然后 Native 会去调用 JS 的方法,获取队列里面所有需要执行的方法。
所以我们需要事先创建好一个 iframe,插入到 DOM 里面,方便后续使用。
callHandler
每次调用的时候只需要复用这个 iframe 就行了。这里是处理 callback 并通知 Native 的代码:
通知 Native 之后,它怎么拿到我们的
handlerName
和data
呢?我们可以实现一个fetchQueue
的方法。然后将其挂载到
window.WebViewJavascriptBridge
对象上面。这样 iOS 就可以使用
evaluateJavaScript
轻松拿到这个messageQueue
。那么 iOS 又是如何回调 JS 的 callback 函数呢?这个其实和 Android 的
onReceive
是同样的原理。这里可以实现一个_handleMessageFromObjC
方法,同样挂载到window.WebViewJavascriptBridge
对象上面,等待 iOS 回调。流程如下:
registerHandler
registerHandler 和 Android 原理是一模一样的,都是提前注册一个事件,等待 iOS 调用,具体就不多讲了,这里直接放代码:
总结
这些就是 Hybrid 里面 JS 和 Native 交互的大致原理,忽略了不少细节,比如初始化
WebViewJavascriptBridge
对象等等,感兴趣的也可以参考一下这个库:JsBridge