iOS 网页异步函数调用

604 阅读2分钟

在网页中调用函数一般使用下列两个方法

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
    }
}