iOS開發WKWebView與JavaScript交互詳解

iOS開發中或多或少會嵌入h5頁面,h5頁面有時需要和原生進行交互:比如h5界面需要通知原生處理一些事情(如拍照等),原生界面需要傳參給h5界面(如拍照的圖片數據);由於iOS現在已棄用UIWebView,今天主要詳細梳理下WKWebView和JavaScript交互細節;

demo準備

完整代碼
爲了說明WKWebView和JavaScript的交互,搭建了一個簡單的界面:


iOS界面:

  • 藍色爲原生button
  • 紅色爲原生label
  • 青色爲webview界面(加載本地html),帶有h5的button
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, width:
view.bounds.width, height: 200))
view.addSubview(webview)
let fileURL = Bundle.main.url(forResource: "iOSTest", withExtension: "html")
webview.loadFileURL(fileURL!, allowingReadAccessTo: Bundle.main.bundleURL)

iOSTest.html代碼:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>iOS WKWebView 交互</title>
</head>

<body style="background-color: aquamarine">

<button id="mm_btn" style="font-size: 17px;" onclick="alert(alertText())">
    web button
</button>

<script>

function alertText() {
  return 'hello js'
}

function jsFunction() {
    return 'CallJSFunction'
}

function jsFunctionParameter(prefix) {
  return prefix + 'CallJSFunction'
}

</script>

</body>

</html>

提示框

h5的button編寫了一個alert彈框的代碼,在瀏覽器上打開點擊按鈕能正常顯示提示框;但在我們這個代碼WKWebView中卻沒任何反應;
這是因爲WKWebView默認是不顯示h5彈框的,需要手動編寫代碼提示原生的彈框;

  1. 設置webview的ui代理
webview.uiDelegate = self
  1. 實現協議,攔截彈框事件;協議方法有3個,對應1個按鈕的提示框,2個按鈕的及帶輸入框的提示框;可以通過回調的方法取得提示框的內容,然後使用原生的提示框:
extension ViewController: WKUIDelegate {
    // 只有一個 按鈕的Alert
    func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        let alertController = UIAlertController(title: "js alert", message: message, preferredStyle: .alert)
        alertController.addAction(
            UIAlertAction(title: "ok", style: .default, handler: { _ in
                completionHandler()
            })
        )

        present(alertController, animated: true)
    }

    // 兩個按鈕的 調用block返回yes或者no來確定是點擊了取消按鈕還是同意按鈕
    func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        // 原生的彈框... completionHandler(true)
    }

    // 帶輸入框的alert completionHandler可以回調給js輸入框輸入的內容
    func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
        // 原生的彈框... completionHandler("xxx")
    }
}

iOS調用JS

WKWebView提供了執行JS的方法evaluateJavaScript

/* @abstract Evaluates the given JavaScript string.
 @param javaScriptString The JavaScript string to evaluate.
 @param completionHandler A block to invoke when script evaluation completes or fails.
 @discussion The completionHandler is passed the result of the script evaluation or an
error.

*/
open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil)

使用比較簡單:

// webview調用js 已有函數
webview.evaluateJavaScript("jsFunction()") {[weak self] (data, _) in
    self?.label.text = data as? String ?? "data error"
}

// webview調用js 已有函數(帶參數)
webview.evaluateJavaScript("jsFunctionParameter('wkWebview')") {[weak self] (data, _)
in
    self?.label.text = data as? String ?? "data error"
}

點擊原生button時調用以上evaluateJavaScript,可以發現能正常調用JS:

iOS注入JS代碼

另一種iOS調用JS的方式是通過添加WKUserScript注入的方式;

let webConfig = WKWebViewConfiguration()
let webUserController = WKUserContentController()
// 注入js
let injectionScript = WKUserScript(source:"jsFunctionParameter('wkWebview')", injectionTime: .atDocumentEnd,
forMainFrameOnly: true)
webUserController.addUserScript(injectionScript)
webConfig.userContentController = webUserController
// 通過自定義config的方式創建webview
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, width:
view.bounds.width, height: 200), configuration: webConfig)
....

以上代碼和之前evaluateJavaScript的方式效果一樣;

evaluateJavaScript和addUserScript的區別

1 . 調用時機不同:
evaluateJavaScript可以在任何需要的時候調用(比如點擊哪個原生的按鈕);而addUserScript注入JS的方式只有2種時機

public enum WKUserScriptInjectionTime : Int {
    // Document加載前
    case atDocumentStart = 0
    // Document加載後
    case atDocumentEnd = 1
}
  1. 作用不同
  • 從2者的API名稱可以看出,evaluateJavaScript側重於調用JS現有的代碼;addUserScript側重於添加JS代碼(即原先JS沒有對應的代碼)
    例如,使用addUserScript可以爲h5動態添加元素:
let script = "var p = document.createElement('p'); p.innerHTML='injection from
WKWebView'; document.getElementsByTagName(\"body\")[0].appendChild(p);"
let injectionScript = WKUserScript(source: script, injectionTim
forMainFrameOnly: true)
webUserController.addUserScript(injectionScript)
webConfig.userContentController = webUserController
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, wi
view.bounds.width, height: 200), configuration: webConfig)
....

重新運行代碼,可以看到webview上已多了個p標籤:

但是對於這點其實也沒有嚴格的要求,兩者都能實現新增JS代碼、調用已有JS代碼;

  • evaluateJavaScript能取得JS的返回值,addUserScript沒有返回值

JS調用iOS

JS調用iOS,WKWebView提供了scriptMessageHandler的API:

  • WKWebView添加scriptMessageHandler
let webConfig = WKWebViewConfiguration()
let webUserController = WKUserContentController()
// 爲防止循環引用,自定義WeakScriptMessageDelegate封裝了一層delegate
let scriptDelegate = WeakScriptMessageDelegate(delegate: self)
webUserController.add(scriptDelegate, name: WebScriptHandlerName.clickButton)
webConfig.userContentController = webUserController
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, widt
view.bounds.width, height: 200), configuration: webConfig)
....
  • JS端messageHandlers需要發送事件通知WKWebView:
// html
<button id="mm_btn" style="font-size: 17px;" onclick="jsCalliOS()">
    web button
</button>

// script
function jsCalliOS() {
    // 無參數
    // postMessage必須帶參數 否則iOS收不到回調,無參數空值如postMessage({})
    window.webkit.messageHandlers.clickButton.postMessage({})
    
    // 帶參數
    window.webkit.messageHandlers.clickButton.postMessage('js_parameter')
}

window.webkit.messageHandlers固定寫法,後面的clickButton爲handler名稱需要和iOS中webUserController.add(scriptDelegate, name: WebScriptHandlerName.clickButton),name一致;

  • iOS端接收JS的回調(實現WKScriptMessageHandler協議方法)處理自己的邏輯,即實現了JS調用iOS
extension ViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == WebScriptHandlerName.clickButton {
            // 無參數
            print("call from js")
            // openCamera()...
            
            // 帶參數 Allowed types are NSNumber, NSString, NSDate, NSArray,NSDictionary,NSNull
            guard let parameter = message.body as? String else { return }
            print("call from js " + parameter)
            // openCamera(parameter)
        }
    }
}

JS調用iOS並獲取iOS的返回值

以上JS調用iOS的交互,存在一個問題;比如調用了iOS代碼並需要得到iOS處理的結果,JS怎麼才能獲取到這個返回值呢?WKWebView貌似並沒有提供相關的API;
這時,我們就需要結合上面的提到過的知識,曲折、間接的實現返回值的功能;

  • 通過提示框的方式實現
    之前提到的,提示框的回調有3種;其中runJavaScriptTextInputPanelWithPrompt這種是能拿到h5提示框的內容,同時還能回傳iOS輸入的內容;這種情況正符合這種JS、iOS交互需求;

h5代碼修改

// script
function jsCalliOS() {
    // prompt彈框 result即爲iOS輸入框completionHandler回調回來的
     var result = prompt('clickButton')
     var p = document.getElementsByTagName('body')[0]
     p.innerHTML = result
}

iOS端代碼

// 帶輸入框的alert completionHandler可以回調給js輸入框輸入的內容
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler:
@escaping (String?) -> Void) {
    if prompt == "clickButton" {
        completionHandler("result from wkwebview")
    }
}

這種方式很好的利用了prompt特性,但是也存在一些問題;如果該h5不是在iOS上打開而是在其他端(瀏覽器、安卓)打開,由於它們不同於iOS的WKWebView,prompt就會正常彈出;因此這種方式還需要額外判斷環境;

  • JS調iOS,再讓iOS調用JS
    JS調用iOS的方式同上;
    JS和iOS 2端約定一個新的function用於接收iOS返回值
// script
function getClickButtonResult(parameter) {
    var p = document.getElementsByTagName('body')[0]
    p.innerHTML = parameter
}

iOS端在收到JS調用回調時,再主動調用剛約定的JS的接收返回值的function:

func userContentController(_ userContentController: WKUserContentController, didReceive
message: WKScriptMessage) {
    if message.name == WebScriptHandlerName.clickButton {
        // js調用webview, webview再調用js  將需要的返回值通過參數帶入
        webview.evaluateJavaScript("getClickButtonResult('result from wkwebview')")
    }
}

這種方式沒有環境問題,但相對來說比較麻煩;

無法修改h5源碼的情況,JS與WKWebView的交互

大部分情況,h5都是自己或者同事編寫的;JS與WKWebView的交互只需要按照上面的方式2端編碼就行;但如果我們用的別人的h5,肯定不會有我們需要的交互代碼;
這時,我們還是可以通過注入JS的方式動態實現:

  • 瀏覽器查看h5源碼,想方設法取到需要交互的節點(如通過id,tag);例如事件的button id=mm_btn,則可以注入以下代碼:
let script = """
            function injectionJsCalliOS() {
                window.webkit.messageHandlers.clickButton.postMessage({})
            };
            var button = document.getElementById('mm_btn');
            button.onclick = injectionJsCalliOS;
            """
let injectionScript = WKUserScript(source: script, injection
forMainFrameOnly: true)
webUserController.addUserScript(injectionScript)
webConfig.userContentController = webUserController

之後,iOS端仍一樣處理:

func userContentController(_ userContentController: WKUserContentController, didReceive
message: WKScriptMessage) {
    if message.name == WebScriptHandlerName.clickButton {
        // 無參數
        print("call from js")
        // openCamera()...
}

WebViewJavascriptBridge

上面我們使用了系統提供的API實現了交互,過程相對繁瑣;
現有一個輕量的第三方庫WebViewJavascriptBridge,能大大簡化交互流程;
github主頁

  • iOS端WKWebView不再需要添加scriptMessageHandler,取而代之使用WebViewJavascriptBridge:
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, width: view.bounds.width, height: 200))
...
webBridge = WebViewJavascriptBridge(webview)

WebViewJavascriptBridge調用JS:

// 無參數
webBridge.callHandler("jsFunction", data: nil) {[weak self] (rps) in
    self?.label.text = rps as? String ?? "data error"
}
// 帶參數
webBridge.callHandler("jsFunction", data: "wkWebview") {[weak self] (rps) in
    self?.label.text = rps as? String ?? "data error"
}

WebViewJavascriptBridge添加scriptMessageHandler供JS調用(responseCallback可以直接返回值給JS,不用向系統API那樣麻煩):

webBridge.registerHandler(WebScriptHandlerName.clickButton) { (data,
responseCallback) in
    print("CalliOSFunction with JSParameter" + (data as? String ?? ""))
    // 返回值給JS
    responseCallback?("result from wkwebview")
}
  • JS代碼
// script
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)
}

setupWebViewJavascriptBridge(function(bridge) {
    /* Initialize your app here */
    bridgeObj = bridge

    // 添加Handler供iOS端調用
    bridge.registerHandler('jsFunction', function(data, responseCallback) {
        //data就是從iOS獲取的值 responseCallback返回給iOS的值
        responseCallback((data === undefined ? '' : data) + 'CallJSFunction')
    })
})

JS調用iOS:

// script
function jsCalliOS() {
    //-----------------------WebViewJavascriptBridge API--------------------------
    
    bridgeObj.callHandler('clickButton', 'js_parameter', function(rsp) {
        var p = document.getElementsByTagName('body')[0]
        p.innerHTML = rsp
    })
    
    //----------------------------------------------------------------------------
}

ps:
Handler的名稱2端同樣需要約定一致;
使用了WebViewJavascriptBridge,不要再手動添加scriptMessageHandler;否則WebViewJavascriptBridge所有交互都會不生效;

擴展

Android WebView與JS的交互

  • WebView調用JS
// Android 4.4
webView.loadUrl("javascript:androidCallJS()");
// Android 4.4以上, 第二個參數爲閉包可以獲取JS返回值
webview.evaluateJavascript("javascript:androidCallJS()", null)
  • JS調用Android
    Android代碼
webView.addJavascriptInterface(new MyJavascriptInterface(this), "injectedObject");
public class MyJavascriptInterface {
    private Context context;

    public MyJavascriptInterface(Context context) {
        this.context = context;
    }

    @JavascriptInterface
    public void clickButton(String data) {
        
    }
}

JS代碼

// script
window.injectedObject. 
clickButton('call from js')

參考:https://www.jianshu.com/p/97f52819a19d

WinForm CefSharp與JS的交互

CefSharp調用JS

webView.ExecuteScriptAsync("CefSharpCallJS()");  

JS調用CefSharp
WinForm代碼

// 將c#對象註冊爲 js對象
public class JsEvent 
{    
  public void ClickButton(string parameter) 
    { 
      .....
    } 
} 

webview.RegisterJsObject("injectedObject", new JsEvent(), false)

JS代碼

// script
injectedObject.ClickButton('call from js')

參考: https://blog.csdn.net/gong_hui2000/article/details/48155547

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章