iOS中WKWebView和Native交互

前言

瞭解本文以前須要準備JS和WebView的一些基礎知識,須要知道JS的基本語法和WebView調用JS的經常使用接口。javascript

iOS實現JS和Native交互的WebView有UIWebView和WKWebView。經過KVC拿到UIWebView的JSContext,經過JSContext實現交互。 WKWebView有了新特性MessageHandler來實現JS調用原生方法。從實現思路是來說,UIWebView和WKWebView是同樣的。 因此,本文只介紹WKWebView上JS和Native的交互思路,UIWebView有需求的能夠模仿實現。html

JS和Native交互經常使用的場景

經常使用的分爲下面幾種場景:java

  • H5獲取Native用戶信息(這種比較簡單,只須要Native注入JS就好了,思路有三種下面介紹)git

  • H5傳遞信息給Native,調用Native分享(這種屬於JS調用Native)github

  • Native告訴H5分享結果(這種屬於Native調用JS)web

下面一一介紹,實現以下:json

H5獲取Native用戶信息

現有用戶信息格式以下,須要注入到JS,供H5調用:swift

let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]

複製代碼
注入JS變量

Native注入JS變量實現以下api

let userContent = WKUserContentController.init()

let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
for key in userInfo.keys {
    let script = WKUserScript.init(source: "var \(key) = \"\(userInfo[key]!)\"", injectionTime: .atDocumentStart, forMainFrameOnly: true)
    userContent.addUserScript(script)
}

let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))
複製代碼

經過遍歷userInfo的keys,把key做爲變量,value做爲String值,注入到JS上下文中。數組

在H5中實現調用以下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <script type="text/javascript">
    function btnClick() {
        try {
            alert(name)
            alert(sex)
            alert(phone)
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>

複製代碼
注入JS對象

Native注入JS對象實現以下

let userContent = WKUserContentController.init()

let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
let jsonData = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted)
let jsonText = String.init(data: jsonData!, encoding: String.Encoding.utf8)

let script = WKUserScript.init(source: "var userInfo = \(jsonText!)", injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContent.addUserScript(script)
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))
複製代碼

經過把userInfo字典轉化成json,做爲對象賦值給userInfo,注入JS上下文中。

在H5中實現調用以下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <script type="text/javascript">
    function btnClick() {
        try {
            alert(JSON.stringify(userInfo))
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>
複製代碼
注入JS函數

Native注入JS函數實現以下

let userContent = WKUserContentController.init()

let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
let jsonData = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted)
let jsonText = String.init(data: jsonData!, encoding: String.Encoding.utf8)

let script = WKUserScript.init(source: "var iOSApp = {\"getUserInfo\":function(){return \(jsonText!)}}", injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContent.addUserScript(script)
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))
複製代碼

經過封裝getUserInfo匿名函數,執行函數return咱們的對象,生成全局對象iOSApp,調用iOSApp.getUserInfo()。 這樣寫的好處是,咱們的H5在調用函數的時候,能夠很容易知道哪些是原生注入,防止和本地形成衝突,便於理解。

在H5中實現調用以下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <script type="text/javascript">
    function btnClick() {
        try {
            alert(JSON.stringify(iOSApp.getUserInfo()))
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>
複製代碼

以上講了三種方式實現用戶信息的傳遞,都是經過WKUserContentController注入JS實現的,實際上我也能夠經過WebView的evaluateJavaScript方法實現注入。

evaluateJavaScript實現注入

一樣的WebView的調用H5,提供了evaluateJavaScript接口,此接口既能夠執行JS函數回調結果,也能夠注入JS。

下面使用接口實現JS函數的注入

let userContent = WKUserContentController.init()
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))

...

//代理方法加載完成
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
    let jsonData = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted)
    let jsonText = String.init(data: jsonData!, encoding: String.Encoding.utf8)

    webView.evaluateJavaScript("var iOSApp = {\"getUserInfo\":function(){return \(jsonText!)}}", completionHandler: nil)
}
複製代碼

在WebView加載完成以後,使用evaluateJavaScript實現了JS函數的注入,H5實現調用正常。

H5傳遞信息給Native,調用Native分享

不少時候H5須要傳遞信息給咱們的Native,咱們Native再執行相應的邏輯。

Native實現代碼以下

let userContent = WKUserContentController.init()
userContent.add(self, name: "shareAction")
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))

...

//代理方法,window.webkit.messageHandlers.xxx.postMessage(xxx)實現發送到這裏
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    print(message.body)
    print(message.name)
    print(message.frameInfo.request)
    
    if message.name == "shareAction" {
        let list = message.body as! [String: String]
        print(list["title"]!)
        print(list["content"]!)
        print(list["url"]!)
    }
}

複製代碼

userContent.add(self, name: "shareAction")本地添加shareAction的接口聲明,當JS調用shareAction回調代理方法,實現參數捕獲(WKScriptMessage)。 這樣咱們本地就獲得了分享的傳參了,而後能夠調用本地SDK實現分享的邏輯了。

H5實現代碼以下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <meta charset="utf-8">
    <script type="text/javascript">
    function btnClick() {
        try {
            window.webkit.messageHandlers.shareAction.postMessage({"title":"分享", "content":"內容", "url":"連接"})
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>
複製代碼

Native告訴H5分享結果

上面實現了JS傳參數給Native,可是Native怎麼告訴H5分享結果呢,下面是實現邏輯。

Native實現以下

let userContent = WKUserContentController.init()
userContent.add(self, name: "shareAction")
let config = WKWebViewConfiguration.init()
config.userContentController = userContent

let wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "http://192.168.2.1/js.html")!))

...

//代理方法,window.webkit.messageHandlers.xxx.postMessage(xxx)實現發送到這裏
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    print(message.body)
    print(message.name)
    print(message.frameInfo.request)
    
    if message.name == "shareAction" {
        let list = message.body as! [Any]
        
        let dict = list[0] as! [String: String]
        print(dict["title"]!)
        print(dict["content"]!)
        print(dict["url"]!)
        
        let shareSucc = list[1] as! String//獲取回調JS,通知H5分享成功了
        let script = "\(shareSucc)(true)"
        wkWebView?.evaluateJavaScript(script, completionHandler: nil)
    }
}
複製代碼

獲取shareSucc的函數回調名稱,在合適的時候咱們能夠經過這個JS函數回調,告訴H5咱們的分享結果。

JS實現以下

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <meta charset="utf-8">
    <script type="text/javascript">
    function shareSucc(isShare) {
        alert(isShare)
    }

    function btnClick() {
        try {
            window.webkit.messageHandlers.shareAction.postMessage([{ "title": "分享", "content": "內容", "url": "連接" }, "shareSucc"])
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="btnClick()" style="font-size: 100px;">test JS</button>
    </p>
</body>

</html>
複製代碼

以前postMessage是發送的字典,因爲咱們的需求增多了,因此仍是改爲數組。 最後發送shareSucc的字符串,告訴Native咱們有一個shareSucc的函數能夠接收分享的結果。

JS和Native統一封裝

上面講了JS回調Native,Native回調JS,實現了咱們經常使用的一些業務邏輯。 裏面有不少重複的代碼,實現起來也不友好,下面咱們把這些重用的所有封裝一下,改爲好用的接口給上層,使Native和JS的開發人員都不用操心太多的實現細節。

H5界面的代碼

<!DOCTYPE html>
<html>

<head>
    <title>js Bridge demo</title>
    <meta charset="utf-8">
    <script type="text/javascript">
    function shareSucc(isShare) {
        alert(isShare)
    }

    function reqUserInfoClick() {
        try {
            alert(iOSApp.getUserInfo())
        } catch (err) {
            alert(err)
        }
    }

    function reqShareClick() {
        try {
            iOSApp.shareAction("分享title", "分享content", "分享url", "shareSucc")
        } catch (err) {
            alert(err)
        }
    }
    </script>
</head>

<body>
    <h1>js demo test</h1>
    <p style="text-align: center;">
        <button type="button" onclick="reqUserInfoClick()" style="font-size: 100px;">獲取用戶信息</button>
        <button type="button" onclick="reqShareClick()" style="font-size: 100px;">執行分享</button>
    </p>
</body>

</html>
複製代碼

構造基礎類JWebViewController

//
//  JWebViewController.swift
//  JSBridgeTest
//
//  Created by jackyshan on 2018/9/26.
//  Copyright © 2018年 GCI. All rights reserved.
//

import UIKit
import WebKit

class JWebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler {

    private var mAsyncScriptArray:[JKWkWebViewHandler] = []
    private var mSyncScriptArray:[JKWkWebViewHandler] = []
    
    private var wkWebView: WKWebView?

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }
    
    public func startUrl(_ url: URL) {
        let configuretion = WKWebViewConfiguration()
        configuretion.preferences = WKPreferences()
        configuretion.preferences.javaScriptEnabled = true
        configuretion.userContentController = WKUserContentController()
        if self.mAsyncScriptArray.count != 0 || self.mSyncScriptArray.count != 0 {
            // 在載入時就添加JS // 只添加到mainFrame中
            let script = WKUserScript(source: createScript(), injectionTime: .atDocumentStart, forMainFrameOnly: true)
            configuretion.userContentController.addUserScript(script)
        }

        //異步須要回調,因此須要添加handler
        for item in self.mAsyncScriptArray {
            configuretion.userContentController.add(self, name: item.name)
        }
        
        let wkWebView = WKWebView(frame: self.view.bounds, configuration: configuretion)
        wkWebView.uiDelegate = self
        self.view.insertSubview(wkWebView, at: 0)
        let request = URLRequest(url: url)
        wkWebView.load(request)
        self.wkWebView = wkWebView
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        //釋放handler
        for item in self.mAsyncScriptArray {
            wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: item.name)
            wkWebView?.configuration.userContentController.removeAllUserScripts()
        }
    }
    
    // MARK: - 添加JS
    public func addAsyncJSFunc(functionName: String, parmers: [String], action: @escaping ([String:AnyObject]) -> Void) {
        var obj = self.mAsyncScriptArray.filter { (obj) -> Bool in
            return obj.name == functionName
        }.first
        
        if obj == nil {
            obj = JKWkWebViewHandler()
            obj!.name = functionName
            obj!.parmers = parmers
            obj!.action = action
            self.mAsyncScriptArray.append(obj!)
        }
    }

    public func addSyncJSFunc(functionName: String, parmers: [String]) {
        var obj = self.mSyncScriptArray.filter { (obj) -> Bool in
            return obj.name == functionName
            }.first
        
        if obj == nil {
            obj = JKWkWebViewHandler()
            obj!.name = functionName
            obj!.parmers = parmers
            self.mSyncScriptArray.append(obj!)
        }
    }
    
    // MARK: - 插入JS
    private func createScript() -> String {
        var result = "iOSApp = {"
        for item in self.mAsyncScriptArray {
            let pars = createParmes(dict: item.parmers)
            let str = "\"\(item.name!)\":function(\(pars)){window.webkit.messageHandlers.\(item.name!).postMessage([\(pars)]);},"
            result += str
        }
        for item in self.mSyncScriptArray {
            let pars = createParmes(dict: item.parmers)
            let str = "\"\(item.name!)\":function(){return JSON.stringify(\(pars));},"
            result += str
        }
        result = (result as NSString).substring(to: result.count - 1)
        result += "}"
        print("++++++++\(result)")
        return result
    }
    
    private func createParmes(dict: [String]) -> String {
        var result = ""
        for key in dict {
            result += key + ","
        }
        if result.count > 0 {
            result = (result as NSString).substring(to: result.count - 1)
        }
        return result
    }

    // MARK: - 執行JS
    public func actionJsFunc(functionName: String, pars: [AnyObject], completionHandler: ((Any?, Error?) -> Void)?) {
        var parString = ""
        for par in pars {
            parString += "\(par),"
        }
        
        if parString.count > 0 {
            parString = (parString as NSString).substring(to: parString.count - 1)
        }
        
        let function = "\(functionName)(\(parString));"
        wkWebView?.evaluateJavaScript(function, completionHandler: completionHandler)
    }

    // MARK: - WKUIDelegate
    public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        let alert = UIAlertController(title: "提示", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "肯定", style: .default, handler: { (_) -> Void in
            // We must call back js
            completionHandler()
        }))
        
        self.present(alert, animated: true, completion: nil)
    }

    // MARK: - WKScriptMessageHandler
    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        
        let funcObjs = self.mAsyncScriptArray.filter { (obj) -> Bool in
            return obj.name == message.name
        }
        
        if let funcObj = funcObjs.first {
            let pars = message.body as! [AnyObject]
            var dict: [String: AnyObject] = [:]
            for i in 0..<funcObj.parmers.count {
                let key = funcObj.parmers[i]
                if pars.count > i {
                    dict[key] = pars[i]
                }
            }
            
            funcObj.action?(dict)
        }
    }
}

class JKWkWebViewHandler: NSObject {
    fileprivate var name:String!
    fileprivate var parmers:[String]!
    fileprivate var action:(([String:AnyObject]) -> Void)?
}

複製代碼

繼承JWebViewController,實現業務

//
//  ViewController.swift
//  JSBridgeTest
//
//  Created by jackyshan on 2018/9/26.
//  Copyright © 2018年 GCI. All rights reserved.
//

import UIKit

class ViewController: JWebViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        let userInfo = ["name": "wb", "sex": "male", "phone": "12333434"]
        let jsonData = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted)
        let jsonText = String.init(data: jsonData!, encoding: String.Encoding.utf8)
        
        //添加getUserInfo腳本,返回用戶信息
        addSyncJSFunc(functionName: "getUserInfo", parmers: [jsonText!])
        
        //添加shareAction腳本,得到分享參數
        addAsyncJSFunc(functionName: "shareAction", parmers: ["name", "sex", "phone", "shareBack"]) { [weak self] (dict) in
            print(dict["name"]!)
            print(dict["sex"]!)
            print(dict["phone"]!)
            
            //執行shareBack腳本,告訴H5分享結果
            self?.actionJsFunc(functionName: dict["shareBack"] as! String, pars: [true as AnyObject], completionHandler: nil)
        }
        
        //開始加載H5
        startUrl(URL.init(string: "http://192.168.2.1/js.html")!)
        
    }
    
}
複製代碼

講解JWebViewController

構造JKWkWebViewHandler類,存儲信息

class JKWkWebViewHandler: NSObject {
    fileprivate var name:String!
    fileprivate var parmers:[String]!
    fileprivate var action:(([String:AnyObject]) -> Void)?
}
複製代碼

添加JS,使用JKWkWebViewHandler存儲

public func addAsyncJSFunc(functionName: String, parmers: [String], action: @escaping ([String:AnyObject]) -> Void) {
    var obj = self.mAsyncScriptArray.filter { (obj) -> Bool in
        return obj.name == functionName
    }.first
    
    if obj == nil {
        obj = JKWkWebViewHandler()
        obj!.name = functionName
        obj!.parmers = parmers
        obj!.action = action
        self.mAsyncScriptArray.append(obj!)
    }
}

public func addSyncJSFunc(functionName: String, parmers: [String]) {
    var obj = self.mSyncScriptArray.filter { (obj) -> Bool in
        return obj.name == functionName
        }.first
    
    if obj == nil {
        obj = JKWkWebViewHandler()
        obj!.name = functionName
        obj!.parmers = parmers
        self.mSyncScriptArray.append(obj!)
    }
}
複製代碼

建立JS腳本,使用iOSApp對象封裝,異步回調傳回Native的函數window.webkit.messageHandlers.xxx直接封裝在JS函數中。 這樣有一個好處,H5調用JS,直接iOSApp.xxx(xxx)就好了,不須要寫window.webkit.messageHandlers.xxx這些代碼。 這對於H5來講,跟平時寫的JS腳本沒有什麼區別,方便了調用。 對於Native來講,幫H5作了JS的回調的封裝,並經過handler回調獲得本身想要的參數,經過這個封裝,兩端的工做都只須要關注 業務層就好了,繼承JWebViewController,能夠專心寫業務邏輯。

private func createScript() -> String {
        var result = "iOSApp = {"
        for item in self.mAsyncScriptArray {
            let pars = createParmes(dict: item.parmers)
            let str = "\"\(item.name!)\":function(\(pars)){window.webkit.messageHandlers.\(item.name!).postMessage([\(pars)]);},"
            result += str
        }
        for item in self.mSyncScriptArray {
            let pars = createParmes(dict: item.parmers)
            let str = "\"\(item.name!)\":function(){return JSON.stringify(\(pars));},"
            result += str
        }
        result = (result as NSString).substring(to: result.count - 1)
        result += "}"
        print("++++++++\(result)")
        return result
}
複製代碼

構造JS,實現傳參給H5頁面

public func actionJsFunc(functionName: String, pars: [AnyObject], completionHandler: ((Any?, Error?) -> Void)?) {
    var parString = ""
    for par in pars {
        parString += "\(par),"
    }
    
    if parString.count > 0 {
        parString = (parString as NSString).substring(to: parString.count - 1)
    }
    
    let function = "\(functionName)(\(parString));"
    wkWebView?.evaluateJavaScript(function, completionHandler: completionHandler)
}
複製代碼

注入JS腳本到WKWebViewConfiguration中

let configuretion = WKWebViewConfiguration()
configuretion.preferences = WKPreferences()
configuretion.preferences.javaScriptEnabled = true
configuretion.userContentController = WKUserContentController()
if self.mAsyncScriptArray.count != 0 || self.mSyncScriptArray.count != 0 {
    // 在載入時就添加JS // 只添加到mainFrame中
    let script = WKUserScript(source: createScript(), injectionTime: .atDocumentStart, forMainFrameOnly: true)
    configuretion.userContentController.addUserScript(script)
}

//異步須要回調,因此須要添加handler
for item in self.mAsyncScriptArray {
    configuretion.userContentController.add(self, name: item.name)
}

let wkWebView = WKWebView(frame: self.view.bounds, configuration: configuretion)
複製代碼

合適的時候釋放JS的handler,注意不釋放的話,Controller不會調用deinit,發生內存泄露。

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    
    //釋放handler
    for item in self.mAsyncScriptArray {
        wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: item.name)
        wkWebView?.configuration.userContentController.removeAllUserScripts()
    }
}
複製代碼

代碼示例放到Github了,有須要的能夠下載查看。

關注我

歡迎關注公衆號:jackyshan,技術乾貨首發微信,第一時間推送。

相關文章
相關標籤/搜索