在WKWebView中从本地代码调用JavaScript函数

62

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

(我可以在configuration.userContentController对象上使用- addScriptMessageHandler:name:来允许JS向本地进行通信,但我正在寻找反向通信。)


一次失败的尝试:我尝试使用webView.loadRequest(NSURLRequest(URL: NSURL(string: "javascript:alert(1)"))),但好像什么都没有发生。 - Sophie Alpert
我为此提交了一个雷达 -- 标记为重复:http://www.openradar.me/radar?id=6415485126049792 - Sophie Alpert
请将其归档为功能请求,提交至 https://bugreport.apple.com/。 - jcesarmobile
1
似乎他们修复了http://trac.webkit.org/changeset/169765 - jcesarmobile
2
另请参阅https://devforums.apple.com/message/986573。 - Tom Hamming
显示剩余3条评论
7个回答

77

我在这里提问后不久就为此提交了一个Radar。

几天前刚刚添加了一种新的方法(感谢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.


您可能还需要在WKWebView中启用Javascript(现在似乎默认情况下已禁用)。let preferences = WKPreferences() preferences.javaScriptEnabled = true let configuration = WKWebViewConfiguration() configuration.preferences = preferences webView = WKWebView(frame: .zero, configuration: configuration) - wildcat12

27

详情

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

结果

在此输入图像描述

资源

文档对象的属性和方法


只是提醒一下,addScript 不再是 WKWebViewConfiguration 的方法了吗? - Firo
我想在我的 webview url 中添加用户名和密码,以便用户自动登录我的应用程序。您能否建议我如何使用您的代码? - Chandni
我不知道你的授权机制是什么,但通常人们使用Alamofire库或UrlSession。另外,你尝试过使用webView.loadUrl(string: "YOUR_URL_WITH_LOGIN_AND_PASSWORD")加载吗? - Vasily Bodnarchuk
不使用config.add,我使用了webview.configuration.userContentController.add(self, name: "handlernamehere")。另外,请确保启用javascript:webview.configuration.preferences.javaScriptEnabled = true。 - Bryan Cimo

7

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

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

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

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

这会重新加载整个页面吗? - Liviu R
1
这样做行不通,因为你正在使用 WKWebView 配置的副本(属性具有复制标志)。唯一可以设置配置的时间是在 WKWebView 初始化时。 - stk

3
这里有个对我很有用的方法:
创建一个 WKWebView 扩展,定义一个名为 'runJavaScriptInMainFrame:' 的方法。在扩展方法中,使用 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链接。

3
使用未经记录或私有API,这会被应用商店审批过程拒绝吗? - LarsJK
1
@Larsaronen,是的,我认为你是正确的:很可能这段代码会导致应用被拒绝。我想测试一些WKWebView中的JavaScript,所以将其用作临时解决方案。应该提到这不应该在生产应用程序中使用。谢谢提醒! - GazingAtNavel

1
有传言称这是一个 bug,因为存在一个类似于 UIWebView 中公开可用的私有函数,可以从 Objective-C 中评估 JavaScript。

是的,有一个私有API可以做到这一点:https://github.com/WebKit/webkit/blob/master/Source/WebKit2/UIProcess/API/Cocoa/WKWebViewPrivate.h#L151 但是为什么要将其设为私有...看起来是故意的。 - LarsJK

1

我刚刚开始研究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的演讲)

1
抱歉 - 我不想在加载时运行脚本,而是在本地端触发某些操作后再运行。 - Sophie Alpert
1
这实际上不起作用; addUserScript 只在页面加载之前起作用。 - Tom Hamming

1

这是一个运行JS代码以从base64字符串加载.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)")
            }
        }
    )
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接