前端工程師所需要了解的WebView
點(diǎn)上方藍(lán)字關(guān)注公眾號「前端UpUp」
JSBridge
JSBridge 簡單來講,主要是 給 JavaScript 提供調(diào)用 Native 功能的接口,讓混合開發(fā)中的『前端部分』可以方便地使用地址位置、攝像頭甚至支付等 Native 功能。
既然是『簡單來講』,那么 JSBridge 的用途肯定不只『調(diào)用 Native 功能』這么簡單寬泛。實(shí)際上,JSBridge 就像其名稱中的『Bridge』的意義一樣,是 Native 和非 Native 之間的橋梁,它的核心是 構(gòu)建 Native 和非 Native 間消息通信的通道,而且是 雙向通信的通道。

所謂 雙向通信的通道:
JS 向 Native 發(fā)送消息 : 調(diào)用相關(guān)功能、通知 Native 當(dāng)前 JS 的相關(guān)狀態(tài)等。
Native 向 JS 發(fā)送消息 : 回溯調(diào)用結(jié)果、消息推送、通知 JS 當(dāng)前 Native 的狀態(tài)等。
JavaScript 是運(yùn)行在一個(gè)單獨(dú)的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。由于這些 Context 與原生運(yùn)行環(huán)境的天然隔離,我們可以將這種情況與 RPC(Remote Procedure Call,遠(yuǎn)程過程調(diào)用)通信進(jìn)行類比,將 Native 與 JavaScript 的每次互相調(diào)用看做一次 RPC 調(diào)用。如此一來我們可以按照通常的 RPC 方式來進(jìn)行設(shè)計(jì)和實(shí)現(xiàn)。

在 JSBridge 的設(shè)計(jì)中,可以把前端看做 RPC 的客戶端,把 Native 端看做 RPC 的服務(wù)器端,從而 JSBridge 要實(shí)現(xiàn)的主要邏輯就出現(xiàn)了:通信調(diào)用(Native 與 JS 通信) 和 句柄解析調(diào)用。(如果你是個(gè)前端,而且并不熟悉 RPC 的話,你也可以把這個(gè)流程類比成 JSONP 的流程。)
通過以上的分析,可以清楚地知曉 JSBridge 主要的功能和職責(zé),接下來,就分析一下在 Android WebView 和 iOS WebView 中實(shí)現(xiàn) Native 與 JS 通信的原理。
Android WebView
Android 4.4前:Android WebView在低版本 & 高版本采用了不同的Webkit版本的內(nèi)核(正因?yàn)槿绱耍琀5的很多新特性,在Android版本小于4.4的安卓機(jī)上,都不支持)
Android 4.4后:原本基于Webkit的WebView開始基于Chromium內(nèi)核,這一改動大大提升了WebView組件的性能以及對HTML5, CSS3, JavaScript的支持。不過它的API卻沒有很大的改動,在兼容低版本的同時(shí)只引進(jìn)了少部分新的API,并不需要你做很大的改動。
在 Android WebView,要實(shí)現(xiàn) JS 調(diào)用 Java,有 3 種方法:
JavascriptInterfaceWebViewClient.shouldOverrideUrlLoading()WebChromeClient.onXXX()
1、JavascriptInterface
這是 Android 提供的 JS 與 Native 通信的官方解決方案。
首先 Native 端需要實(shí)現(xiàn)這么一個(gè)類,給 JavaScript 調(diào)用。
public class WebAppInterface {public void showToast(String toast) {Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();}}
WebAppInterface類,通過以下代碼,添加到 WebView 的 JavaScriptInterface 中。WebView webView = (WebView) findViewById(R.id.webview);webView.addJavascriptInterface(new WebAppInterface(this), "Android");// 這里的Android會被當(dāng)做一個(gè)變量,注入到頁面的window中。
接著就可以在 JS 中調(diào)用 Native 了。
function showAndroidToast(toast) {Android.showToast(toast);}
2、WebViewClient.shouldOverrideUrlLoading()
這個(gè)方法的作用是攔截所有 WebView 的 URL Scheme 。
URL Scheme 是一種類似于 url 的鏈接,是為了方便 app 直接互相調(diào)用設(shè)計(jì)的,形式和普通的 url 近似,主要區(qū)別是 protocol 和 host 一般是自定義的。
攔截 URL Scheme 的主要流程是:Web 端通過某種方式(例如 iframe.src/location.href)發(fā)送 URL Scheme 請求,之后 Native 攔截到請求并根據(jù) URL Scheme(包括所帶的參數(shù))進(jìn)行相關(guān)操作。
頁面可以構(gòu)造一個(gè)特殊格式的 URL Scheme 觸發(fā),shouldOverrideUrlLoading 攔截 URL 后判斷其格式,然后 Native 就能執(zhí)行自身的邏輯了。
public class CustomWebViewClient extends WebViewClient {public boolean shouldOverrideUrlLoading(WebView view,String url) {if (isJsBridgeUrl(url)) {// JSbridge的處理邏輯return true;}return super.shouldOverrideUrlLoading(view, url);}}
3、WebChromeClient.onXXX()
通過修改原來瀏覽器的 window某些方法,然后攔截固定規(guī)則的參數(shù),然后分發(fā)給Java 對應(yīng)的方法去處理
alert,可以被 WebView 的 WebChromeClient.onJsAlert() 監(jiān)聽
confirm,可以被 WebView 的 WebChromeClient.onJsConfirm() 監(jiān)聽
console.log,可以被 WebView 的 WebChromeClient.onConsoleMessage() 監(jiān)聽
prompt,可以被 WebView 的
WebChromeClient.onJsPrompt()監(jiān)聽
prompt 簡單舉例說明,Web 頁面通過調(diào)用 prompt()方法,安卓客戶端通過監(jiān)聽WebChromeClient.onJsPrompt()事件,攔截傳入的參數(shù),如果參數(shù)符合一定協(xié)議規(guī)范,那么就解析參數(shù),扔給后續(xù)的 Java 去處理。
window.prompt(message, value);WebChromeClient.onJsPrompt()就會受到回調(diào)。onJsPrompt()方法的 message參數(shù)的值正是JS的方法 window.prompt()的 message的值。
public class CustomWebChromeClient extends WebChromeClient {public boolean onJsPrompt(WebView view,String url,String message,String defaultValue,JsPromptResult result) {// 處理JS 的調(diào)用邏輯result.confirm();return true;}}
Java 調(diào)用 JavaScript
Android,在 Kitkat(4.4)只能用 loadUrl 一段 JavaScript 代碼。
webView.loadUrl("javascript:" + javaScriptString);而 Kitkat 之后的版本,也可以用 evaluateJavascript 方法實(shí)現(xiàn):
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {public void onReceiveValue(String value) {// native代碼}});
IOS WebView
In apps that run in iOS 8 and later, use the WKWebView class instead of using UIWebView. Additionally, consider setting the WKPreferences property javaScriptEnabled to NO if you render files that are not supposed to run JavaScript.
在 IOS8 之前,蘋果手機(jī)的 WebView 使用的 UIWebView,UIWebView長期以來存在某些問題:
加載速度慢
存在內(nèi)存泄漏
內(nèi)存占用多,內(nèi)存優(yōu)化困難
如果內(nèi)存占用過多還可能因?yàn)檎加眠^多被系統(tǒng)kill掉
在 WWDC 2014 大會上,IOS8推出了 WKWebView,WKWebView 是現(xiàn)代 Webkit API 在 iOS 8 和 OS X Yosemite 應(yīng)用中的核心部分。它代替了 UIKit 中的 UIWebView 和 AppKit 中的 WebView,提供了統(tǒng)一的跨雙平臺 API。擁有 60fps 滾動刷新率、內(nèi)置手勢、高效的 app 和 web 信息交換通道、和 Safari 相同的 JavaScript 引擎。
JavaScript ?? Swift 對話機(jī)制
使用用戶腳本來注入 JavaScript
WKUserScript 允許在正文加載之前或之后注入到頁面中。這個(gè)強(qiáng)大的功能允許在頁面中以安全且唯一的方式操作網(wǎng)頁內(nèi)容。
一個(gè)簡單的例子如下,用戶改變背景的用戶腳本被插入到網(wǎng)頁中:
let source = "document.body.style.background = \"#777;// 注入腳本 在文檔加載完成后執(zhí)行let userScript = WKUserScript()let userScript = WKUserScript(source: source, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)let userContentController = WKUserContentController()userContentController.addUserScript(userScript)let configuration = WKWebViewConfiguration()configuration.userContentController = userContentControllerself.webView =WKWebView(frame: self.view.bounds, configuration: configuration)
對象可以以 JavaScript 源碼形式初始化,初始化時(shí)還可以傳入是在加載之前還是結(jié)束時(shí)注入,以及腳本影響的是這個(gè)布局還是僅主要布局。于是用戶腳本被加入到 WKUserContentController 中,并且以 WKWebViewConfiguration 屬性傳入到 WKWebView 的初始化過程中。
這個(gè)樣例可以簡單擴(kuò)展為更為高級的頁面修改方法,例如去除廣告、隱藏評論等。
Message Handlers
利用以下代碼,可以跟Native進(jìn)行通信
window.webkit.messageHandlers.{NAME}.postMessage()Handler的name可以通過 WKScriptMessageHandler 協(xié)議中的 addScriptMessageHandler() 接口函數(shù)設(shè)置:
class NotificationScriptMessageHandler: NSObject, WKScriptMessageHandler {func userContentController(userContentController: WKUserContentController,didReceiveScriptMessage message: WKScriptMessage!) {println(message.body)}}let userContentController = WKUserContentController()let handler = NotificationScriptMessageHandler()userContentController.addScriptMessageHandler(handler, name: "notification")
于是當(dāng)通知進(jìn)入 app 的時(shí)候,比如說在頁面中創(chuàng)建一個(gè)新對象,相關(guān)信息就可以這樣傳遞:
window.webkit.messageHandlers.notification.postMessage({body: '發(fā)送給Native'});添加用戶腳本來對 web 事件監(jiān)聽并用 Message Handler 將信息傳回 app。
總結(jié)
通信原理是 JSBridge 實(shí)現(xiàn)的核心,實(shí)現(xiàn)方式可以各種各樣,但是萬變不離其宗。這里,推薦的實(shí)現(xiàn)方式如下:
JavaScript 調(diào)用 Native 推薦使用 注入 API 的方式。( iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式。)
Native 調(diào)用 JavaScript 則直接執(zhí)行拼接好的 JavaScript 代碼即可。
對于其他方式,諸如 React Native、微信小程序 的通信方式都與上描述的近似,并根據(jù)實(shí)際情況進(jìn)行優(yōu)化。
以 React Native 的 iOS 端舉例:JavaScript 運(yùn)行在 JSCore 中,實(shí)際上可以與上面的方式一樣,利用注入 API 來實(shí)現(xiàn) JavaScript 調(diào)用 Native 功能。不過 React Native 并沒有設(shè)計(jì)成 JavaScript 直接調(diào)用 Object-C,而是 為了與 Native 開發(fā)里事件響應(yīng)機(jī)制一致,設(shè)計(jì)成 需要在 Object-C 去調(diào) JavaScript 時(shí)才通過返回值觸發(fā)調(diào)用。原理基本一樣,只是實(shí)現(xiàn)方式不同。


