在 WKWebview 的使用过程中,与 JS 的交互是最经典的一个话题。常规的 JS 方法调用原生、原生调用 JS 方法,在 WKWebview 的 API 中都有支持,这里就不讨论了。
本文讨论如何在 JS 方法中获得原生方法的返回数据,目前可行的有 3 种典型方式:
- 基于 window.prompt 方法的同步返回方式
- 基于 postMessage 发送回调标记模式
- 基于 postMessage 的 Promise 处理模式
基于 window.prompt 方法的同步返回方式
在 JS 中调用 window.prompt 会弹出一个输入提示框,输入内容后会将结果同步返回给方法。在 WKWebview 的 WKUIDelegate 代理方法中实现 runJavaScriptTextInputPanelWithPrompt 方法,可以拦截到输入的内容,并通过回调方法进行返回。
// JS
let msg = window.prompt("msg", "msgPlaceholder")
// Swift
func webView(_ webView: WKWebView,
runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (String?) -> Void) {
completionHandler("result")
}
这个方法看似很好,但是直接破坏了 window.prompt 的原始语义,对于需要采用标准的交互模式来使用的场景就被破坏了,尽管可以通过 prompt 传递的信息来区分使用场景,但是对于规范的使用要求非常高,很容易就覆盖了正常行为。
基于 postMessage 发送回调标记模式
JS 调用原生使用的是这个方法: window.webkit.messageHandlers..postMessage
postMessage 发送后就收不到回调了,剩下就是原生代码独自执行,但是仍然有办法可以把数据传会给 JS。
基本思路就是 JS 在发送 postMessage 时,顺便带上一个标记,原生收到这个标记,获取 JS 端需要的数据,再调用 JS 端已有的一个方法将数据带过去,通过标记匹配到 JS 端能响应的回调方法,在回调方法里接受数据。
例如下面的示例,在 JS 端保存一个回调方法的对象 nativeHandlers,通过一个 ID (即回调标记)映射所有的回调方法,在发送 postMessage 时保存回调方法,并给原生回调 ID,原生响应时调用 nativeCallback JS 方法将回调 ID 与原生信息一同带回,JS 即能响应此回调。
// JS
class NativeInteraction {
constructor() {
this.nativeHandlers = {}
this.getNativeInfo = this.getNativeInfo.bind(this)
this.nativeCallback = this.nativeCallback.bind(this)
}
getNativeInfo(handler) {
let uuid = "123"
this.nativeHandlers[uuid] = handler
window.webkit.messageHandlers.nativeHandler.postMessage(`${uuid}`);
}
nativeCallback(uuid, info) {
let handler = this.nativeHandlers[uuid]
if (handler) {
handler(info)
delete this.nativeHandlers[uuid]
}
}
}
NativeInteraction.shared = new NativeInteraction()
function jsTest() {
NativeInteraction.shared.getNativeInfo((info) => {
console.log(info)
})
}
// Swift
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case "nativeHandler":
guard let uuid = message.body as? String else { return }
let info = "native info"
let script = """
NativeInteraction.shared.nativeCallback("\(uuid)", "\(info)")
"""
self.wkwebview.evaluateJavaScript(script)
default:
return
}
}
基于 postMessage 的 Promise 处理模式
postMessage 方法返回的是一个 Promise,在 iOS 14 以前的版本中,Promise 的 resolve 方法是无法获得返回值的:
let result = window.webkit.messageHandlers.nativeHandler.postMessage(null);
result
.then((value) => {
console.log("promise result:", value) // value 始终为 null
})
.catch((error) => {
console.log("error:", error)
})
不过即使这样,也是能发挥一些小小的用处的。在原生端处理消息的代理方法中执行 evaluateJavaScript 方法,可以将数据设置到 JS 中的某个属性中,并且能在 resolve 中获取到:
// JS
let nativeValue = null
let result = window.webkit.messageHandlers.nativeHandler.postMessage(null);
result
.then((value) => {
console.log("promise result:", value) // value 始终为 null
console.log(nativeValue) //
})
.catch((error) => {
console.log("error:", error)
})
// Swift
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case "nativeHandler":
let result = ""
self.wkwebview.evaluateJavaScript("nativeValue = \(result)")
default:
return
}
}
evaluateJavaScript 方法会先执行完成,然后才是执行 .then 中的 resolve 方法,如果对于回调的要求不高可以采用这个方法。如果这个方法的最大弊端有 2 点:
- 需要使用全局变量
- 无法在原生端异步拿一些数据再返回,didReceive 方法执行完后 resolve 方法就要执行了。
在 iOS 14 中,苹果终于大发慈悲地顺手(tuo le hen jiu)解决了这个问题,提供了一个新的方法监听 JS 发送的消息:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void)
这其中的关键就是 replyHandler 发挥的作用,通过它就可以把数据传递给 JS,并在 resolve 中获得:
// Swift
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) {
switch message.name {
case "nativeHandler":
let result = ""
replyHandler("result", nil)
default:
return
}
}
// JS
window.webkit.messageHandlers.nativeHandler.postMessage(null)
.then((value) => {
console.log("promise result:", value) // value 现在有值了
})
.catch((error) => {
console.log("error:", error)
})
根据你的使用场景,可以在以上方法的基础上做些修改,例如第 2 种方法可以将回调集合换成 Promise 的集合,效果相同。总体上思路都不外乎这 3 种。