iOS下JS与Swift互相调用(三)WKScriptMessageHandler

使用 WKWebView 的时候,如果想要实现 JS 调用iOS原生的方法,除了拦截URL之外,还有一种简单的方式,那就是利用 WKWebView 的新特性WKScriptMessageHandler 来实现 JS 调用原生方法

1.WKScriptMessageHandler 是什么

这是苹果文档的说明:

A class conforming to the WKScriptMessageHandler protocol provides a method for receiving messages from JavaScript running in a webpage.

WKScriptMessageHandler 是一个协议,这个协议提供一个方法可以接收 JS 传过来的消息,这个协议方法里面有两个类 WKScriptMessageWKUserContentController

1
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)

WKScriptMessage 就是 JS 传给 Swift 的消息,有两个主要属性:

1
2
3
4
5
6
7
/*! @abstract The body of the message.
@discussion Allowed types are NSNumber, NSString, NSDate, NSArray,
NSDictionary, and NSNull. */
open var body: Any { get }
/*! @abstract The name of the message handler to which the message is sent. */
open var name: String { get }

WKUserContentController 可以理解为调度控制器,有两个主要的方法:

1
2
3
4
5
//添加供 JS 调用 Swift 的桥梁,这里的name对应WKScriptMessage中的name,一般情况下我们认为它就是方法名
func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String)
//这个方法可以向 JS 里面注入我们的 JS 方法
func addUserScript(_ userScript: WKUserScript)

要使用 WKScriptMessageHandler 的功能,JS 需要这样调用:

1
2
3
4
5
6
7
window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
//其中<name>,就是上面方法里的第二个参数`name`。
//例如下面我们调用API的时候第二个参数填@"Share",那么在JS里就是:
//window.webkit.messageHandlers.Share.postMessage(<messageBody>)
//<messageBody>是一个键值对,键是body,值可以有多种类型的参数。
// 在`WKScriptMessageHandler`协议中,我们可以看到mssage是`WKScriptMessage`类型,有一个属性叫body。
// 而注释里写明了body 的类型:Allowed types are NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull.

2.使用WKScriptMessageHandler

WKWebView 有一个 WebViewConfiguration 类型的参数,而WKWebViewConfiguration 有一个属性叫userContentController,它是WKUserContentController类型的参数,我们就需要使用这个 userContentController 参数

WKWebView 实例化的时候配置 WKWebViewConfiguration 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func setupWKWebView() {
let configuration = WKWebViewConfiguration()
configuration.userContentController = WKUserContentController()
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true
preferences.minimumFontSize = 40.0
configuration.preferences = preferences
self.wkWebView = WKWebView(frame: self.view.frame, configuration: configuration)
let urlStr = Bundle.main.path(forResource: "index.html", ofType: nil)
let url = URL(fileURLWithPath: urlStr!)
self.wkWebView?.loadFileURL(url, allowingReadAccessTo: url)
self.wkWebView?.uiDelegate = self
self.view.addSubview(self.wkWebView!)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
// addScriptMessageHandler 很容易导致循环引用
// 控制器 强引用了WKWebView,WKWebView copy(强引用了)configuration, configuration copy (强引用了)userContentController
// userContentController 强引用了 self (控制器)
self.wkWebView?.configuration.userContentController.add(self as WKScriptMessageHandler, name: "ScanAction")
self.wkWebView?.configuration.userContentController.add(self as WKScriptMessageHandler, name: "Location")
self.wkWebView?.configuration.userContentController.add(self as WKScriptMessageHandler, name: "Share")
self.wkWebView?.configuration.userContentController.add(self as WKScriptMessageHandler, name: "Color")
self.wkWebView?.configuration.userContentController.add(self as WKScriptMessageHandler, name: "Pay")
self.wkWebView?.configuration.userContentController.add(self as WKScriptMessageHandler, name: "Shake")
self.wkWebView?.configuration.userContentController.add(self as WKScriptMessageHandler, name: "GoBack")
self.wkWebView?.configuration.userContentController.add(self as WKScriptMessageHandler, name: "PlaySound")
}

为了防止循环引用无法释放控制器需要在 viewWillDisappear 里面移除 MessageHandler

1
2
3
4
5
6
7
8
9
10
11
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
self.wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "ScanAction")
self.wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "Location")
self.wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "Share")
self.wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "Color")
self.wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "Pay")
self.wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "Shake")
self.wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "GoBack")
self.wkWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "PlaySound")
}

3.实现协议方法

这里除了需要实现两个协议:WKUIDelegate, WKScriptMessageHandler ,实现WKUIDelegate 协议是因为 JS 里面需要 alert,实现 WKScriptMessageHandler 就是这篇文章需要讨论的 JS 调用 Swift 的原生方法,下面是实现协议的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//MARK: - WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case "ScanAction":
print("saoyisao")
case "Location":
getLocation()
case "Share":
share(params: message.body as! Dictionary)
case "Color":
changeBackGroundColor(params: message.body as! Dictionary)
case "Pay":
pay(params: message.body as! Dictionary)
case "Shake":
sharkeAction()
case "GoBack":
goBack()
case "PlaySound":
palySound(soundName: message.body as! String)
default:
break
}
}

根据 WKScriptMessage 的关键属性 name 来区分执行不同的方法,body 中存着JS 要给 Swift 传的参数

这是 HTML 和 JS 部分代码:

1
2
3
4
5
6
7
8
9
10
<h1>这是按钮调用</h1>
<input type="button" value="扫一扫" onclick="scanClick()" />
<input type="button" value="获取定位" onclick="locationClick()" />
<input type="button" value="修改背景色" onclick="colorClick()" />
<input type="button" value="分享" onclick="shareClick()" />
<input type="button" value="支付" onclick="payClick()" />
<input type="button" value="摇一摇" onclick="shake()" />
<input type="button" value="返回" onclick="goBack()" />
<input type="button" value="播放声音" onclick="playSound()" />
<h1>这是文件上传</h1>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function scanClick() {
window.webkit.messageHandlers.ScanAction.postMessage(null);
}
function shareClick() {
window.webkit.messageHandlers.Share.postMessage({title:'测试分享的标题',content:'测试分享的内容',url:'http://m.rblcmall.com/share/openShare.htm?share_uuid=shdfxdfdsfsdfs&share_url=http://m.rblcmall.com/store_index_32787.htm&imagePath=http://c.hiphotos.baidu.com/image/pic/item/f3d3572c11dfa9ec78e256df60d0f703908fc12e.jpg'});
}
function locationClick() {
window.webkit.messageHandlers.Location.postMessage(null);
}
function setLocation(location) {
asyncAlert(location);
document.getElementById("returnValue").value = location;
}
function getQRCode(result) {
asyncAlert(result);
document.getElementById("returnValue").value = result;
}
function colorClick() {
window.webkit.messageHandlers.Color.postMessage({r:'67',g:'205',b:'128',a:'0.5'});
}
function payClick() {
window.webkit.messageHandlers.Pay.postMessage({order_no:'201511120981234',channel:'wx',amount:'1',subject:'粉色外套'});
}
function payResult(str) {
asyncAlert(str);
document.getElementById("returnValue").value = str;
}
function shareResult(channel_id,share_channel,share_url) {
var content = channel_id+","+share_channel+","+share_url;
asyncAlert(content);
document.getElementById("returnValue").value = content;
}

4.JS和Swift的相互调用

我们来看一下 JS 调用 Swift 的 share 方法,这个方法里的参数就是来自 WKScriptMessagebody 属性:

1
2
3
4
5
6
7
8
9
10
func share(params:[String: String]) {
let title = params["title"] ?? ""
let content = params["content"] ?? ""
let url = params["url"] ?? ""
let jsStr = "shareResult('\(String(describing: title))','\(String(describing: content))','\(String(describing: url))')"
self.wkWebView?.evaluateJavaScript(jsStr, completionHandler: { (result, error) in
print("\(String(describing: result))---\(String(describing: error))")
})
}

Swift 调用 JS 的方法还是使用 WKWebview的 evaluateJavaScript 方法:

1
2
3
4
let jsStr = "shareResult('\(String(describing: title))','\(String(describing: content))','\(String(describing: url))')"
self.wkWebView?.evaluateJavaScript(jsStr, completionHandler: { (result, error) in
print("\(String(describing: result))---\(String(describing: error))")
})

使用 WKScriptMessageHandler 的优点:

  • 在 JS 中写起来简单,不用再用创建URL的方式那么麻烦
  • JS 传递参数更方便,使用拦截URL的方式传递参数,只能把参数拼接在后面,如果遇到要传递的参数中有特殊字符,如&、=、?等,必须得转换,否则参数解析会出错,如下例子:

如果传递地址是这样的:

1
http://www.baidu.com/share/openShare.htm?share_uuid=shdfxdfdsfsdf&name=1234556

使用拦截 URL 的JS调用方式如下:

1
loadURL("firstClick://shareClick?title=分享的标题&content=分享的内容&url=链接地址&imagePath=图片地址"); }

将上面的 url 放入链接地址这里后,根本无法区分share_uuid是其他参数,还是url里附带的参数,使用MessageHandler 就可以避免特殊字符引起的问题

原文地址: iOS下 JS 与 OC 互相调用(三)–MessageHandler

原文是讨论 OC 与 JS 的交互,我按照作者的思路,用 Swift 实现 Native 与 JS 的交互,这是 代码地址

本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!