31

是否可以使用 WKWebView 在 JavaScript 和 Swift/Obj-C 本机代码之间进行同步通信?

这些是我尝试过但失败的方法。

方法 1:使用脚本处理程序

WKWebView接收JS消息的新方式是使用userContentController:didReceiveScriptMessage:从JS调用的委托方法,window.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?') 这种方法的问题是在执行原生委托方法的过程中,JS执行没有被阻塞,所以我们不能返回一个值立即调用webView.evaluateJavaScript("something = 42", completionHandler: nil).

示例(JavaScript)

var something;
function getSomething() {
    window.webkit.messageHandlers.myMsgHandler.postMessage("What's the meaning of life, native code?"); // Execution NOT blocking here :(
    return something;
}
getSomething();    // Returns undefined

示例(斯威夫特)

func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
    webView.evaluateJavaScript("something = 42", completionHandler: nil)
}

方法 2:使用自定义 URL 方案

在 JS 中,重定向 usingwindow.location = "js://webView?hello=world"调用本地 WKNavigationDelegate方法,其中可以提取 URL 查询参数。但是,与 UIWebView 不同的是,委托方法不会阻止 JS 执行,因此立即调用evaluateJavaScript将值传回 JS 在这里也不起作用。

示例(JavaScript)

var something;
function getSomething() {
    window.location = "js://webView?question=meaning" // Execution NOT blocking here either :(
    return something;
}
getSomething(); // Returns undefined

示例(斯威夫特)

func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler decisionHandler: (WKNavigationActionPolicy) -> Void) {
    webView.evaluateJavaScript("something = 42", completionHandler: nil)
    decisionHandler(WKNavigationActionPolicy.Allow)
}

方法 3:使用自定义 URL 方案和 IFRAME

这种方法仅在window.location分配方式上有所不同。不是直接分配它,而是使用src空的属性iframe

示例(JavaScript)

var something;
function getSomething() {
    var iframe = document.createElement("IFRAME");
    iframe.setAttribute("src", "js://webView?hello=world");
    document.documentElement.appendChild(iframe);  // Execution NOT blocking here either :(
    iframe.parentNode.removeChild(iframe);
    iframe = null;
    return something;
}
getSomething();

尽管如此,这也不是一个解决方案,它调用与方法 2 相同的本机方法,它不是同步的。

附录:如何使用旧的 UIWebView 实现这一点

示例(JavaScript)

var something;
function getSomething() {
    // window.location = "js://webView?question=meaning" // Execution is NOT blocking if you use this.

    // Execution IS BLOCKING if you use this.
    var iframe = document.createElement("IFRAME");
    iframe.setAttribute("src", "js://webView?question=meaning");
    document.documentElement.appendChild(iframe);
    iframe.parentNode.removeChild(iframe);
    iframe = null;

    return something;
}
getSomething();   // Returns 42

示例(斯威夫特)

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    webView.stringByEvaluatingJavaScriptFromString("something = 42")    
}
4

5 回答 5

10

不,由于 WKWebView 的多进程架构,我不相信这是可能的。WKWebView 与您的应用程序在同一进程中运行,但它与在其自己的进程中运行的 WebKit 通信(现代 WebKit API 简介)。JavaScript 代码将在 WebKit 进程中运行。所以本质上你要求在两个不同的进程之间进行同步通信,这违背了他们的设计。

于 2014-12-03T12:34:07.713 回答
4

我发现了一个进行同步通信的技巧,但还没有尝试过:https ://stackoverflow.com/a/49474323/2870783

编辑:基本上您可以使用 JS prompt() 将您的有效负载从 js 端传送到本机端。在原生的 WKWebView 中将不得不拦截提示调用并判断是普通调用还是 jsbridge 调用。然后,您可以将结果作为对提示调用的回调返回。因为提示调用是以等待用户输入的方式实现的,所以您的 javascript-native 通信将是同步的。缺点是您只能通过字符串进行交流。

于 2018-04-18T12:54:28.357 回答
3

我也调查过这个问题,和你一样失败。要解决此问题,您必须将 JavaScript 函数作为回调传递。本机函数需要评估回调函数以返回结果。实际上,这是 JavaScript 的方式,因为 JavaScript 从不等待。阻塞 JavaScript 线程可能会导致 ANR,这非常糟糕。

我创建了一个名为XWebView的项目,它可以在原生和 JavaScript 之间建立桥梁。它提供绑定样式的 API,用于从 JavaScript 调用本机,反之亦然。有一个示例应用程序。

于 2015-04-28T16:57:28.050 回答
1

可以通过轮询当前 RunLoop 的 acceptInput 方法来同步等待 evaluateJavaScript 的结果。这样做是允许您的 UI 线程在您等待 Javascript 完成时响应输入。

在盲目地将其粘贴到代码中之前,请阅读警告

//
//  WKWebView.swift
//
//  Created by Andrew Rondeau on 7/18/21.
//

import Cocoa
import WebKit

extension WKWebView {
    func evaluateJavaScript(_ javaScriptString: String) throws -> Any? {

        var result: Any? = nil
        var error: Error? = nil
        
        var waiting = true
        
        self.evaluateJavaScript(javaScriptString) { (r, e) in
            result = r
            error = e
            waiting = false
        }
        
        while waiting {
            RunLoop.current.acceptInput(forMode: RunLoop.Mode.default, before: Date.distantFuture)
        }
        
        if let error = error {
            throw error
        }
        
        return result
    }
}

发生的情况是,在执行 Javascript 时,线程调用 RunLoop.current.acceptInput 直到等待为假。这允许您的应用程序的 UI 响应。

一些警告:

  • UI 上的按钮等仍会响应。如果您不希望有人在您的 Javascript 运行时按下按钮,您可能应该禁用与您的 UI 交互。(如果您使用 Javascript 调用另一台服务器,情况尤其如此。)
  • 调用 evaluateJavaScript 的多进程特性可能比您预期的要慢。如果您正在调用“即时”的代码,如果您在紧密循环中重复调用 Javascript,事情可能仍然会变慢。
  • 我只在主 UI 线程上测试过这个。我不知道这段代码将如何在后台线程上工作。如果有问题,请使用 NSCondition 进行调查。
  • 我只在 macOS 上测试过这个。我不知道这是否适用于iOS。
于 2021-07-19T00:49:56.447 回答
0

我遇到了类似的问题,我通过存储承诺回调解决了这个问题。

您通过 WKUserContentController::addUserScript 在 Web 视图中加载的 js

var webClient = {
    id: 1,
    handlers: {},
};

webClient.onMessageReceive = (handle, error, data) => {
    if (error && webClient.handlers[handle].reject) {
        webClient.handlers[handle].reject(data);
    } else if (webClient.handlers[handle].resolve){
        webClient.handlers[handle].resolve(data);
    }

    delete webClient.handlers[handle];
};

webClient.sendMessage = (data) => {
    return new Promise((resolve, reject) => {
       const handle = 'm' + webClient.id++;
       webClient.handlers[handle] = { resolve, reject };
       window.webkit.messageHandlers.<message_handler_name>.postMessage({data: data, id: handle});
   });
}

像这样执行 Js 请求

webClient.sendMessage(<request_data>).then((response) => {
...
}).catch((reason) => {
...
});

在 userContentController 中接收请求:didReceiveScriptMessage

使用 webClient.onMessageReceive(handle, error, response_data) 调用 evaluateJavaScript。

于 2019-04-02T06:43:06.707 回答