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

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容