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

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