Hybrid 開發之 WebView 交互

WebView 是咱們在 app 當中所提供給 web 頁面的瀏覽器環境。 在 iOS 當中,具體是 WKWebView,或者 UIWebView。因爲 UIWebView 即將退出歷史舞臺,咱們這裏只討論 WKWebView,也就是 WebKit 這個系統框架。javascript

在 Hybrid 開發當中,最爲關鍵的是原生與 js 之間的交互。而經過交互過程,最主要是擴展 web 頁面所不具別的原生能力。vue

原生與JS之間的交互

原生調用 JS

咱們知道,在 iOS 中,甚至在 safari 中,js 都是經過 JavascriptCore 執行的。咱們只要獲取到 web 頁面的執行上下文 JSContext,就能夠操控 JS 。java

在 UIWebView 時代,咱們能夠這樣獲取 JSContext:web

let jsContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext")
複製代碼

而 WKWebView 中,JSContext 運行在不一樣的進程當中。咱們只能經過 WKWebView 的其餘方法實現。swift

直接/當即調用

WKWebView 提供了十分便捷的方法,來執行 JS 的方法。api

open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil)
複製代碼

值得注意的是:數組

  • 執行 js 代碼的運行環境是,web 頁面的 全局執行上下文。因此不論是否包含 window.,它調用/操做的都是window 的函數/變量。
  • 執行的過程是異步的,由於實際 JS 的執行是在不一樣的進程中執行的。而 completionHandler 老是在主線程的,能夠放心執行 UI 操做。

若是須要傳遞參數,須要先轉化爲 JSON 字符串瀏覽器

let data = JSON(dataToJs).rawString()!
webView?.evaluateJavaScript("jsFunc(\(data))", completionHandler: { (ret, err) in
    debugPrint(err ?? ret ?? "")
})
複製代碼
注入腳本

某些狀況下,咱們須要注入一些 js 代碼到 web 頁面的運行環境當中。緩存

let scrip = WKUserScript(source: sourceCode, injectionTime: .atDocumentStart, forMainFrameOnly: false)
webView?.configuration.userContentController.addUserScript(scrip)
複製代碼

這一過程實際上與直接執行一段 JS 效果是同樣的,但時機比較早,而能夠在特定時機時執行:bash

.atDocumentStart: Inject the script after the document element has been created, but before any other content has been loaded.

若是注入的時機是.atDocumentStart ,它是除了DOM 樹建立以外,其餘資源加載完成以前。也就是說,早於其餘任何 js 的執行。

.atDocumentEnd Inject the script after the document has finished loading, but before any subresources may have finished loading.

值得注意的是,它還能夠注入到全部 frame 當中

js 的執行時機

爲了理解原生在調用 JS 時候,尤爲是注入 JS 時候的執行時機。咱們在 web 頁面入口文件中添加下列代碼。

console.log('main.js');
document.addEventListener('readystatechange',function() {
	console.log('document.readyState: ' + document.readyState);
})
document.addEventListener('DOMContentLoaded',function() {
	console.log('DOMContentLoaded');
})
window.addEventListener('load',function() {
	console.log('window.load');
})
複製代碼

能夠看到控制檯的輸出是這樣的:

[Log] webView didStartProvisionalNavigation a=3
[Log] webView decidePolicyFor navigationResponse a=3
[Log] .atDocumentStart
[Log] main.js
[Log] home vue mounted 
[Log] document.readyState: interactive
[Log] DOMContentLoaded
[Log] .atDocumentEnd
[Log] document.readyState: complete
[Log] image load finish 
[Log] window.load
[Log] webView didCommit a=undefined
[Log] webView didFinish navigation a=undefined b=89
複製代碼

能夠看到,執行順序大概是這樣的:

  • webView.load 加載頁面
  • 觸發 WKNavigationDelegatewebView:didStartProvisionalNavigation:webView:decidePolicyFor:navigationResponse 方法
  • 執行當 injectionTime == .atDocumentStart時,注入的 JS
  • 加載並執行其餘 js。譬如 vue 中的 main.js 就是在這時候執行的。
  • 首頁 mounted 事件
  • document.readyState 的狀態爲 interactive
  • document DOMContentLoaded 事件,DOM 加載完成
  • 執行當 injectionTime == .atDocumentEnd時,注入的 JS
  • document.readyState 的狀態爲 complete
  • 圖片等其餘資源加載完成
  • window.load
  • 頁面加載完成。觸發 WKNavigationDelegatewebView:didCommit:webView:didFinish:navigation 方法

值得注意的是:

  • 每一次刷新頁面,執行的上下文是不同的。在用 evaluateJavaScript 執行 js 方法時,必須保證頁面加載完成,不然使用注入 JS 的方式。
  • 頁面每次的刷新,都會執行一遍所注入的 js 。若是屢次注入同一 js ,會在加載過程當中執行屢次。因此注入 JS 儘可能在初始化 webView ,獲取調用webView.load 以前。
JS 調用原生的方法
messageHandlers 方式

在 JS 中,咱們能夠向指定的方法發送消息。

window.webkit.messageHandlers.myNativeMethod.postMessage(message)
複製代碼

其中,webkit.messageHandlers 是在 WKWebview 當中纔有的,專門用於處理原生與 web 頁面之間數據傳遞。

而 myNativeMethod 這個方法,須要先在原生代碼當中註冊(注入)。

webView?.configuration.userContentController.add(self, name: "myNativeMethod")
複製代碼

當 web 頁面 postMessage 的時候,WKScriptMessageHandler 的下列方法,會觸發。

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == "myNativeMethod" {
        let body = message.body
        self.myNativeMethod(body)
    }
}
複製代碼

若是方法名匹配的話,咱們就能夠調用指定的原生方法了。

攔截請求的方式

和 UIWebView 時代的方法同樣。咱們能夠經過攔截 web 頁面的 document 請求來達到調用原生方法的目的。當 web 頁面中經過改變 document 的 location 來通知原生,傳遞數據,以此來實現調用原生方法的目的。

document.location = "myApp://myNativeMethod?a=324&b=dhsjg67"
複製代碼

這個過程,能夠理解爲一次請求。其中myApp 是咱們定義的協議名;myNativeMethod 相似於網絡請求的子路徑,表示要執行的方法名。後面的 query 是須要傳遞的參數。

而後,咱們能夠在 WKNavigationDelegate 的 webView:decidePolicyFor navigationAction:decisionHandler 方法中,監聽到這個請求

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    let url = navigationAction.request.url
    if url?.host?.lowercased() == "myapp"{
        if url?.host?.lowercased() == "myNativeMethod" {
            var params = [String:String]()
            url?.query?.components(separatedBy: "&").forEach({
                let arr = $0.components(separatedBy: "&")
                params[arr[0]] = arr.count >= 1 ? arr[1] : ""
            })
            self.myNativeMethod(params)
        }
        decisionHandler(.cancel)
    } else {
        decisionHandler(.allow)
    }
}
複製代碼

當咱們我發現,請求的協議名是咱們的,而且匹配到方法名時,就能夠調用相應的原生方法了。

值得注意的是,當發現是咱們專門用於 web 頁面交互的協議時,須要把這個請求取消掉。否則 web 頁面會所以拋出加載異常。

固然,咱們也能夠經過 iframe 來實現這個過程。方法是先添加一個不會展現的 iframe 元素。

window.messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
document.documentElement.appendChild(messagingIframe);
複製代碼

當須要調用原生方法時

messagingIframe.src = "myApp://myNativeMethod"
複製代碼
WebViewJavascriptBridge

WebViewJavascriptBridge 是咱們在作與 web 頁面交互時,常用的框架。因爲它包含了三端的代碼,能夠大大提交咱們的開發效率。

原生調用JS

首先須要在 web 頁面的 JS 代碼中,註冊方法:

setupWebViewJavascriptBridge(bridge => {
  bridge.registerHandler('jsFunc',(data, responseCallback) =>{
    console.log('jsFunc');
    console.log(data);
    responseCallback("suceess");
  })
})
複製代碼

其中setupWebViewJavascriptBridge 是 WebViewJavascriptBridge 的初始化過程。全部操做必須在初始化完成以後進行:

function setupWebViewJavascriptBridge(callback) {
		if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
		if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
		window.WVJBCallbacks = [callback];
		var WVJBIframe = document.createElement('iframe');
		WVJBIframe.style.display = 'none';
		WVJBIframe.src = 'https://__bridge_loaded__';
		document.documentElement.appendChild(WVJBIframe);
		setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
	}
複製代碼

而後,是在原生端調用:

self.bridge = WKWebViewJavascriptBridge(for: webView)
 
 self.bridge?.callHandler("jsFunc", data: dataToJs, responseCallback: { (result) in
    debugPrint("jsFunc:\(result ?? "")")
})
複製代碼
JS調用原生

首先,仍是先註冊

self.bridge?.registerHandler("nativeFunc", handler: { (data, callBack) in
    debugPrint("nativeFunc:\(data ?? "")")
    callBack?(dataToJs)
})
複製代碼

而後,在 web 頁面中調用

window.WebViewJavascriptBridge.callHandler('nativeFunc',data)
複製代碼
基本原理

在 WebViewJavascriptBridge 中,web 與原生交互都被抽象爲發送/接受消息。Web 頁面向原生髮送消息,都是經過改變 DOM 的 location 實現的。惟一不一樣的是,它使用的是 iframe。

消息類型 協議名 舊版協議名 子路徑
初始化消息 https wvjbscheme bridge_loaded
普通消息 https wvjbscheme wvjb_queue_message

當收到初始化消息時,原生端會注入 JS ,建立用於橋架的 WebViewJavascriptBridge JS對象。

當收到普通消息時,並非經過 URL query 來獲取請求參數。而是經過一個 專門的 JS 方法。

WebViewJavascriptBridge._fetchQueue();
複製代碼

這樣作的好處顯而易見。比 URL query 方式能夠傳遞更爲複雜的數據結構。

值得注意的是,因爲 js 向原生髮送消息過程是異步的。有可能從發送到接受過程當中,發送了多個請求。因此必需要有個消息隊列,也就是一個數組,用於緩存消息。在接受數據的時候再進行拆包和狀況消息隊列。

原生向 JS 發送消息,也是經過特定的 JS 方法:

WebViewJavascriptBridge._handleMessageFromObjC();
複製代碼

參考資料

判斷document加載過程的幾個不一樣方法

相關文章
相關標籤/搜索