LEGO-SDK
LEGO-SDK 是一个轻量级的 WebView 增强库,点击链接以查看使用方法以及开源代码。
前言
这篇文章是《LEGO-SDK 轻量级的跨平台 Web App 引擎》的后续小记。
本文对常见的 WebView 与 Native 交互方式进行描述,并详解 LEGO-SDK 的技术选型。
Javascript
在开始描述交互方式前,我们不得不先普及一下 Javascript 的冷知识。
来历
1995 年,它作为网景浏览器(Netscape Navigator)的一部分首次发布,网景给这个新语言命名为 LiveScript。一年后,为了搭上当时媒体热炒 Java 的顺风车,临时改名为了 JavaScript (当然,Java 和 JavaScript 的关系,就和雷锋和雷锋塔一样 —— 并没有什么关系)
ECMAScript
1996 年,网景将 JavaScript 提交给 ECMA International(欧洲计算机制造商协会) 进行标准化,并最终确定出新的语言标准,它就是 ECMAScript。自此,ECMAScript 成为所有 JavaScript 实现的基础,不过,由于 JavaScript 名字的历史原因和市场原因(很显然 ECMAScript 这个名字并不令人喜欢……),现实中我们只用 ECMAScript 称呼标准,平时都还是使用 JavaScript 来称呼这个语言。
ES3 ~ ES5
不过,JavaScript 开发者们并不怎么在乎这些,因为在诞生之后的 15 年里,ECMAScript 并没有多少变化,而且现实中的很多实现都已经和标准大相径庭。其实在第一版的 ECMAScript 发布后,很快又跟进发布了两个版本,但是自从 1999 年 ECMAScript 3 发布后,十年内都没有任何改动被成功添加到官方规范里。取而代之的,是各大浏览器厂商们争先进行自己的语言拓展,web 开发者们别无选择只能去尝试并且支持这些 API。即使是在 2009 年 ECMAScript 5 发布之后,仍然用了数年这些新规范才得到了浏览器的广泛支持,可是大部分开发者还是写着 ECMAScript 3 风格的代码,并不觉得有必要去了解这些规范。
目前,iOS 与 Android 的 WebView 支持 ES5。
ES6
到了 2012 年,事情突然开始有了转变。大家开始推动停止对旧版本 IE 浏览器的支持,用 ECMAScript 5 (ES5) 风格来编写代码也变得更加可行。与此同时,一个新的 ECMAScript 规范也开始启动。到了这时,大家开始逐渐习惯以对 ECMAScript 规范的版本支持程度来形容各种 JavaScript 实现。在正式被指名为 ECMAScript 第 6 版 (ES6) 之前,这个新的标准原本被称为 ES.Harmony(和谐)。2015 年,负责制定 ECMAScript 规范草案的委员会 TC39 决定将定义新标准的制度改为一年一次,这意味着每个新特性一旦被批准就可以添加,而不像以往一样,规范只有在整个草案完成,所有特性都没问题后才能被定稿。因此,ECMAScript 第 6 版在六月份公布之前,又被重命名为了 ECMAScript 2015(ES2015)
当前,只有 Android 支持部分 ES6 语法,但是,开发者可以使用 Babel 进行兼容处理。
WebView
WebView 的知识也需要普及一下。
WebView 在 UIKit 和 Android UI 中都存在。其中,iOS 的 WebView 又分为 UIWebView 和 WKWebView。 Android 中的 WebView 则统称 WebView,Android 4.4 以前的版本,使用存在缺陷的 WebKit 内核,之后的版本使用 Blink 内核。
UIWebView / iOS
UIWebView 使用 WebKit 内核,Javascript 引擎使用 JavascriptCore。
在 iOS8 之前,Apple 只允许开发者使用 UIWebView 加载 Web 页面,并且不允许开发者自行封装 Javascript 引擎。UIWebView 以其内存占用极高,渲染超慢,Javascript 执行效率超低著称(然而,Safari 并不是使用UIWebView)。同时,UIWebView 还存在 leaks 的风险。
但是,UIWebView 还是有其用武之处的。由先,UIWebView 是一个经长期验证的 WebView,其周边已经存在不少优秀的开源库可供使用。其次,由于应用较早,BUG也较少,非常适合追求稳定性的应用使用。
WKWebView / iOS
WKWebView 同样使用 WebKit 内核,但是 Javascript 引擎使用 Nitro。
在 iOS8 开始,Apple 就允许开发者使用 WKWebView 了(Safari 正是使用该 UI)。WKWebView 自称拥有 60 FPS 的渲染、刷新速度,内存占用仅为 UIWebView 的 20%(确实如此),Javascript 执行效率是 JavascriptCore 的数倍(确实很快)。并且,WKWebView leaks 很稀有。
不过,WKWebView 对于开发者而言,还是** 太 **陌生了。
WKWebView 基础的方法与 UIWebView 是一致的,但 delegate 方法则作了完全的改造。 同时,WKWebView 再也不能使用 JSProtocol / JavascriptCore 进行交互了。
WebView / Android
WebView 在 Android 中有如干面包,虽然很难吃,但是,饿的时候,你还是不得不吃。
WebView 在 Android 中的渲染效率不高,Javascript 的执行速度也比 iOS 慢一大截(CPU性能相同情况下)。不过,Android 的 WebView 兼容性还是可以的,对 ES5/6 的支持最为及时。总体而言 Android WebView 是可以使用的,但自从爆出 Javascript 可以执行任意原生代码的漏洞后,业界对其则是又爱又恨。
JavascriptBridge
要使 Javascript 与 Native 进行交互,需要解决两个问题。
- 捕捉 Javascript 发送到 Native 的事件
- 从 Native 发送事件到 Javascript
同时,我们还需要解决一些使用上的问题。
- 将指定的 Javascript 代码注入至目标网页
- 在网页执行前就注入代码(即是网页可以在任意位置调用指定脚本)
Handle Request
一种传统的方式是捕捉请求。
在 iOS WebView 中具体的方法是:
1.实现 WebView 的 delegate,然后实现 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
回调。
2.拿到 request 并 return false。
3.接下来对 request 中的 Path 或者 HTTPBody 进行处理,得到交互信息。
4.处理请求,并使用[[UIWebView new] stringByEvaluatingJavaScriptFromString:@"xxx"]
发送结果至 WebView。
在 Android 中也有类似的方法,在这里不再详尽解析。
这种方法的优点是简单快捷,只需要在 Web 中建立一个 iframe 标签,并将请求发送至该标签即可。
但是缺点也很明显,request 的发送需要经过多层处理,效率很低,并且,有可能因为某些原因导致请求丢失。
在开源库 JavascriptBridge 中也有类似的实现。
NSURLProtocol
另一种方式是使用 NSURLProtocol 。
使用 NSURLProtocol 可以捕捉到当前应用的所有网络请求(WKWebView除外),不管这个请求是使用 NSURLConnection 还是 NSURLSession 再或是 UIWebView 发出的。
因此,我们可以拦截这个请求,并修改其返回的结果。
在 WebView 中,只需要发送一个 Ajax 请求,即可达到目的。
在 Android 中,有类似的 WebView 方法。
这种方法优点是效率比 Handle Request 高,但仍然逃不出 Request 的陷阱。同时,这种方法对于 WKWebView 是无效的(除非使用 GCDWebServers 进行请求代理)。
JavascriptCore
还有一种方式是使用 JavascriptCore,自 iOS7 开始,苹果允许开发者使用 JavascriptCore 将代码注入 WebView,这种方式效率非常高,并且交互起来非常方便。
使用 JavascriptCore 可以将 JS 对象与 OC 对象进行无缝的转换。
具体用法,请戳 http://nshipster.cn/javascriptcore/
在 Android 中,则是使用 JavascriptInterface 实现的,但是Android 4.2 以下有漏洞,慎用。
这种方法优点是效率很高,使用方便,但是不支持 WKWebView。
WKUserContentController
如果使用 WKWebView,可以使用 WKUserContentController 进行交互。
1.使用 - (void)addUserScript:(WKUserScript *)userScript
注入代码
2.使用 - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name
捕捉请求
3.使用 [[WKWebView new] evaluateJavaScript:@"" completionHandler:nil];
发送结果至 WebView
这种方法优点是支持 WKWebView,但是不支持同步请求,只能异步返回结果。
console.log 注入
在 Android 中,由于存在 JavascriptInterface 漏洞,我们需要避免使用该方式。
1.将高风险接口清除。
removeJavascriptInterface("searchBoxJavaBridge_");
removeJavascriptInterface("accessibility");
removeJavascriptInterface("accessibilityTraversal");
2.继承 WebChromeClient
,重写 public boolean onConsoleMessage(ConsoleMessage consoleMessage)
方法。
3.在consoleMessage中捕捉请求,并使用 evaluateJavascript()
返回结果至WebView。
通过该方法获取请求,可以绕过JavascriptInterface漏洞,Web 调用方法比较简单。
Javascript 指定代码注入
在业务上,我们需要将代码注入至 Web 页面最顶部,以便开发者可以在任意位置发起请求。
如上文所述,UIWebView 可以使用 JSProtocol 的方式,WKWebView 可以使用 addUserScript 的方式。
而 Android 则需要特殊处理,Android 仍然需要使用 JavascriptInterface,但是,该对象不包含 Context,这样可以避免安全问题。
class LGOJavaScriptBridge {
LGOJavaScriptBridge() {}
@JavascriptInterface
public String bridgeScript() {
return "Your script.";
}
}
...
addJavascriptInterface(new LGOJavaScriptBridge(), "JSBridge");
Web 开发者只需要在网页头部添加 script 即可。
<script>JSBridge && eval(JSBridge.bridgeScript())</script>
LEGO-SDK
LEGO-SDK 在 iOS 中,对于 UIWebView 使用 JavascriptCore 进行封装,对于 WKWebView 使用 WKUserContentController 进行封装,对于 Android 则使用 console.log 注入进入封装。
如果你有更好的想法,欢迎提出。
本文只是一些经验之谈,如果有不正确的地方,也欢迎指点评批。
网友评论