在网页中调用函数一般使用下列两个方法
open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil)
open func evaluateJavaScript(_ javaScriptString: String) async throws -> Any
调用方式
同步
比如JS侧有一个函数
function add(x, y) {
return x + y;
}
在原生侧调用为
// 获取到网页容器
let webView = getWebView()
webView.evaluateJavaScript("window.add(1, 2);") { v, e in
// v == 3
// do something
}
// iOS 13
Task {
let v = await webView.evaluateJavaScript("window.add(1, 2)")
// v == 3
}
这种情况比较常见,但有一种异步JS方法的调用,想获取返回值却有些不同。
异步
比如JS侧有个函数
/// 1秒后返回结果
function async add(x, y) {
return new Promise((r, j) => {
setTimeout(r(x+y), 1000);
})
}
在原生侧不同系统(iOS 15, iOS 14, iOS 13及以下)有几种调用方法,如下
- iOS 15
func callAsyncJavaScript(_ functionBody: String,
arguments: [String : Any] = [:],
in frame: WKFrameInfo? = nil,
contentWorld: WKContentWorld) async
- iOS 14
func callAsyncJavaScript(_ functionBody: String,
arguments: [String : Any] = [:],
in frame: WKFrameInfo? = nil,
in contentWorld: WKContentWorld,
completionHandler: ((Result<Any, Error>) -> Void)? = nil)
- iOS 13
iOS 13及以下,没有直接可以运行JS异步函数的方法,需要结合addScriptMessageHandler
以下为封闭的分类,封装了调用JS同步、异步(回调&协和)方法。
public extension WKWebView {
@available(iOS 13.0, *)
func kdEvaluate(_ js: String, _ isAsyncJs: Bool = false) async throws -> Any {
try await withCheckedThrowingContinuation({ check in
kdEvaluate(js, isAsyncJs) { rs in
check.resume(with: rs)
}
})
}
func kdEvaluate(_ js: String, _ isAsyncJs: Bool = false, _ completeHandler: @escaping EvaluateComplete) {
isAsyncJs ?
_kdEvaluateAsync(js: js, completeHandler) :
_kdEvaluateSync(js: js, completeHandler)
}
private func _kdEvaluateAsync(js: String, _ completeHandler: @escaping EvaluateComplete) {
if #available(iOS 15.0, *) {
Task {
let value = try await callAsyncJavaScript(js, contentWorld: .page)
completeHandler(.success(value as Any))
}
} else if #available(iOS 14.0, *) {
callAsyncJavaScript(js, in: nil, in: .page, completionHandler: completeHandler)
} else {
let tmpName = "\(js)-\(Date().timeIntervalSince1970)".md5
let mh = MessageHandler { [weak self] _, message in
self?.configuration.userContentController.removeScriptMessageHandler(forName: tmpName)
guard let msg = message.body as? [String: Any],
let data = msg["data"] else {
completeHandler(.failure(NSError(domain: js, code: -1)))
return
}
completeHandler(.success(data))
}
configuration.userContentController.add(mh, name: tmpName)
let wrap = """
((async () => {\(js)})())
.then(value => {
window.webkit.messageHandlers.\(tmpName).postMessage({data: value})
})
.catch(err => {
window.webkit.messageHandlers.\(tmpName).postMessage({})
})
"""
_kdEvaluateSync(js: wrap, { rs in
if case .failure(_) = rs {
completeHandler(rs)
}
})
}
}
private func _kdEvaluateSync(js: String, _ completeHandler: @escaping EvaluateComplete) {
evaluateJavaScript(js) { value, err in
if let err {
completeHandler(.failure(err))
} else {
completeHandler(.success(value as Any))
}
}
}
typealias EvaluateComplete = (Result<Any, Error>) -> Void
private class MessageHandler: NSObject, WKScriptMessageHandler {
var handler: (WKUserContentController, WKScriptMessage) -> Void
init(handler: @escaping (WKUserContentController, WKScriptMessage) -> Void) {
self.handler = handler
}
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
handler(userContentController, message)
}
}
}
- 代码中的有一处
md5方法
import CommonCrypto
public extension Data {
var md5: String {
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
_ = withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
CC_MD5(bytes.baseAddress, CC_LONG(self.count), &digest)
}
return digest.map { String(format: "%02x", $0) }.joined()
}
}
public extension String {
var md5: String {
guard let data: Data = self.data(using: .utf8) else {
return self
}
return data.md5
}
}