【问题标题】:Call JavaScript function from native code in WKWebView从 WKWebView 中的本机代码调用 JavaScript 函数
【发布时间】:2014-07-25 19:20:57
【问题描述】:

在 iOS 8 中使用 WKWebView,我如何从本机端运行 JavaScript 函数或以其他方式从本机端与 JavaScript 通信?似乎没有类似于 UIWebView 的stringByEvaluatingJavaScriptFromString: 的方法。

(我可以在configuration.userContentController 对象上使用- addScriptMessageHandler:name: 来允许从JS 到本机的通信,但我正在寻找相反的方向。)

【问题讨论】:

标签: javascript ios webkit wkwebview


【解决方案1】:

(我在这里提出问题后不久就为此提交了雷达。)

前几天刚刚添加了一个新方法(感谢jcesarmobile指出):

添加-[WKWebView evaluateJavaScript:completionHandler:]
http://trac.webkit.org/changeset/169765

该方法适用于 iOS 8 beta 3 及更高版本。这是新的方法签名:

/* @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. 
*/ 
- (void)evaluateJavaScript:(NSString *)javaScriptString
         completionHandler:(void (^)(id, NSError *))completionHandler; 

文档可在此处获得:https://developer.apple.com/documentation/webkit/wkwebview/1415017-evaluatejavascript

【讨论】:

  • 为什么不结束这个话题并选择你的答案作为正确的答案,让读者知道有一个答案:-)
  • 我想在我的 webview url 中添加用户名和密码,以便用户自动登录。我的应用程序你能建议我如何使用你的代码。
  • 您可能还需要在 WKWebView 中启用 Javascript(现在似乎默认禁用)。 let preferences = WKPreferences() preferences.javaScriptEnabled = true let configuration = WKWebViewConfiguration() configuration.preferences = preferences webView = WKWebView(frame: .zero, configuration: configuration)
【解决方案2】:

详情

  • Xcode 9.1,Swift 4
  • Xcode 10.2 (10E125)、Swift 5

说明

脚本被插入到将在 WKWebView 中显示的页面中。此脚本将返回页面 URL(但您可以编写另一个 JavaScript 代码)。这意味着脚本事件是在网页上生成的,但它会在我们的函数中处理:

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {...}

解决方案

extension WKUserScript {
    enum Defined: String {
        case getUrlAtDocumentStartScript = "GetUrlAtDocumentStart"
        case getUrlAtDocumentEndScript = "GetUrlAtDocumentEnd"

        var name: String { return rawValue }

        private var injectionTime: WKUserScriptInjectionTime {
            switch self {
                case .getUrlAtDocumentStartScript: return .atDocumentStart
                case .getUrlAtDocumentEndScript: return .atDocumentEnd
            }
        }

        private var forMainFrameOnly: Bool {
            switch self {
                case .getUrlAtDocumentStartScript: return false
                case .getUrlAtDocumentEndScript: return false
            }
        }

        private var source: String {
            switch self {
            case .getUrlAtDocumentEndScript, .getUrlAtDocumentStartScript:
                return "webkit.messageHandlers.\(name).postMessage(document.URL)"
            }
        }

        fileprivate func create() -> WKUserScript {
            return WKUserScript(source: source,
                                injectionTime: injectionTime,
                                forMainFrameOnly: forMainFrameOnly)
        }
    }
}

extension WKWebViewConfiguration {
    func add(script: WKUserScript.Defined, scriptMessageHandler: WKScriptMessageHandler) {
        userContentController.addUserScript(script.create())
        userContentController.add(scriptMessageHandler, name: script.name)
    }
}

用法

初始化 WKWebView

 let config = WKWebViewConfiguration()
 config.add(script: .getUrlAtDocumentStartScript, scriptMessageHandler: self)
 config.add(script: .getUrlAtDocumentEndScript, scriptMessageHandler: self)

 webView = WKWebView(frame:  UIScreen.main.bounds, configuration: config)
 webView.navigationDelegate = self
 view.addSubview(webView)

捕捉事件

extension ViewController: WKScriptMessageHandler {
    func userContentController (_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if  let script = WKUserScript.Defined(rawValue: message.name),
            let url = message.webView?.url {
                switch script {
                    case .getUrlAtDocumentStartScript: print("start: \(url)")
                    case .getUrlAtDocumentEndScript: print("end: \(url)")
                }
        }
    }
}

完整代码示例

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

    private var webView = WKWebView()

    override func viewDidLoad() {
        super.viewDidLoad()

        let config = WKWebViewConfiguration()
        config.add(script: .getUrlAtDocumentStartScript, scriptMessageHandler: self)
        config.add(script: .getUrlAtDocumentEndScript, scriptMessageHandler: self)

        webView = WKWebView(frame:  UIScreen.main.bounds, configuration: config)
        webView.navigationDelegate = self
        view.addSubview(webView)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        webView.load(urlString: "http://apple.com")
    }
}

extension ViewController: WKScriptMessageHandler {
    func userContentController (_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if  let script = WKUserScript.Defined(rawValue: message.name),
            let url = message.webView?.url {
                switch script {
                    case .getUrlAtDocumentStartScript: print("start: \(url)")
                    case .getUrlAtDocumentEndScript: print("end: \(url)")
                }
        }
    }
}

extension WKWebView {
    func load(urlString: String) {
        if let url = URL(string: urlString) {
            load(URLRequest(url: url))
        }
    }
}

extension WKUserScript {
    enum Defined: String {
        case getUrlAtDocumentStartScript = "GetUrlAtDocumentStart"
        case getUrlAtDocumentEndScript = "GetUrlAtDocumentEnd"

        var name: String { return rawValue }

        private var injectionTime: WKUserScriptInjectionTime {
            switch self {
                case .getUrlAtDocumentStartScript: return .atDocumentStart
                case .getUrlAtDocumentEndScript: return .atDocumentEnd
            }
        }

        private var forMainFrameOnly: Bool {
            switch self {
                case .getUrlAtDocumentStartScript: return false
                case .getUrlAtDocumentEndScript: return false
            }
        }

        private var source: String {
            switch self {
            case .getUrlAtDocumentEndScript, .getUrlAtDocumentStartScript:
                return "webkit.messageHandlers.\(name).postMessage(document.URL)"
            }
        }

        fileprivate func create() -> WKUserScript {
            return WKUserScript(source: source,
                                injectionTime: injectionTime,
                                forMainFrameOnly: forMainFrameOnly)
        }
    }
}

extension WKWebViewConfiguration {
    func add(script: WKUserScript.Defined, scriptMessageHandler: WKScriptMessageHandler) {
        userContentController.addUserScript(script.create())
        userContentController.add(scriptMessageHandler, name: script.name)
    }
}

Info.plist

添加 Info.plist 传输安全设置

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

结果

资源

Document Object Properties and Methods

【讨论】:

  • 仅供参考 addScript 不是(不再?)WKWebViewConfiguration 上的方法。
  • 我想在我的 webview url 中添加用户名和密码,以便用户自动登录。我的应用程序你能建议我如何使用你的代码。
  • 我不知道你的授权机制,但通常人们使用Alamofire库或UrlSession。附言您是否尝试加载 webView.loadUrl(string: "YOUR_URL_WITH_LOGIN_AND_PASSWORD")
  • 我没有使用 config.add,而是使用了 webview.configuration.userContentController.add(self, name: "handlernamehere") 另外,请确保启用 javascript: webview.configuration.preferences.javaScriptEnabled = true
【解决方案3】:

这可能不是一个理想的方法,但根据您的用例,您可以在感染用户脚本后重新加载 WKWebView:

NSString *scriptSource = @"alert('WKWebView JS Call!')";

WKUserScript *userScript = [[WKUserScript alloc] initWithSource:scriptSource
                                                  injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
                                               forMainFrameOnly:YES];

[wkWebView.configuration.userContentController addUserScript:userScript];
[wkWebView reload];

【讨论】:

  • 这会重新加载整个页面吗?
  • 这不起作用,因为您正在处理 WKWebView 配置的副本(属性具有复制标志)。只有在 WKWebView 初始化时才能设置配置。
【解决方案4】:

这对我有用:

在定义“runJavaScriptInMainFrame:”方法的 WKWebView 上创建一个扩展。在扩展方法中,使用 NSInvocationOperation 调用未记录的 '_runJavaScriptInMainFrame:' 方法。

extension WKWebView {
    func runJavaScriptInMainFrame(#scriptString: NSString) -> Void {
        let selector : Selector = "_runJavaScriptInMainFrame:"
        let invocation = NSInvocationOperation(target: self, selector: selector, object: scriptString)
        NSOperationQueue.mainQueue().addOperation(invocation)
    }
}

要使用,请致电:

webview.runJavacriptInMainFrame:(scriptString: "some javascript code")

感谢 Larsaronen 提供 WKWebView 私有 API 的链接。

【讨论】:

  • 在应用商店审批流程中使用未记录/私有 API 是否会被拒绝?
  • @Larsaronen,是的,我认为你是对的:这段代码很可能会导致应用程序被拒绝。我希望在 WKWebView 中测试一些 JavaScript,因此将其用作临时修复。应该提到这不应该在生产应用程序中使用。谢谢提醒!
【解决方案5】:

我自己刚开始研究 WKWebView API,所以这可能不是最好的方法,但我认为您可以使用以下代码来实现:

NSString *scriptSource = @"console.log('Hi this is in JavaScript');";

WKUserScript *userScript = [[WKUserScript alloc]
    initWithSource:scriptSource
    injectionTime:WKUserScriptInjectionTimeAtDocumentStart 
    forMainFrameOnly:YES];

[myWKController addUserScript:userScript];

(来自 WWDC'14 演讲)

【讨论】:

  • 抱歉——我不想在加载时运行脚本,但后来为了响应本机端的某些操作。
  • 这实际上是行不通的; addUserScript 的东西只在页面加载之前有效。
【解决方案6】:

有传言说这是一个错误,因为有一个类似于 UIWebView 中公开可用的私有函数(?),用于从 obj-C 评估 javascript。

【讨论】:

【解决方案7】:

方法示例,运行 JS 代码从 base64String 加载 .pdf

  private func setData(_ base64String: String) {
    webView.evaluateJavaScript(
        "externalInterfaceLoadBase64('\(base64String)', scale = 1.0)",
        completionHandler: { _, error in
            if let error = error {
                debugPrint("Evaluating JavaScript error: \(error)")
            }
        }
    )
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多