【问题标题】:JavaScript synchronous native communication to WKWebViewJavaScript 同步原生通信到 WKWebView
【发布时间】:2014-11-10 19:38:42
【问题描述】:

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

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

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

WKWebView接收JS消息的新方式是使用userContentController:didReceiveScriptMessage:的委托方法window.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?')从JS调用 这种方式的问题是,在执行原生委托方法的过程中,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 中,使用 window.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 的方式上有所不同。不是直接分配它,而是使用空iframesrc 属性。

示例 (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")    
}

【问题讨论】:

  • 利用异步特性不是更好的方法(就用户体验而言)吗?例如。在您向 webkit 发布的消息中,提供有关应该从 webkit 发布操作“结果”的位置的信息。例如。 WebKit 可以使用 stringByEvaluatingJavaScriptFromString 调用 gotSomething() javascript 方法?
  • 假设您想询问本机代码是否安装了应用程序。如果你使用同步调用,你可以做一些简单的事情,比如var isTwitterInstalled = isInstalled('twitter');。如果您只有异步调用,则必须将其拆分为两个函数;一个从 JavaScript 调用,另一个从本机代码调用,传递结果。
  • 是的,但优点是,如果执行您试图在 iOS 世界中运行的代码需要任何时间,您的 JavaScript 根本不会等待它,这使得用户界面响应更快。这种方式类似于Node。
  • 我在这里遇到的问题是,似乎不可能调用 JavaScript 并使用该调用的结果 * 来响应 UI/框架请求。例如,打印或复印。我尝试通过完成处理程序进行同步都导致了死锁。
  • 我注意到您的示例代码使用了局部变量,但您评估的 JS 可能不在同一范围内运行。您是否尝试过使用 window 上的变量?

标签: swift ios8 wkwebview


【解决方案1】:

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

【讨论】:

  • 对于像 WKWebView 这样闪亮的新 API 缺少 Android 多年来拥有的功能,真是太可惜了。
  • 这毫无意义。为什么那个 postMessage 方法不能返回一个承诺?
【解决方案2】:

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

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

【讨论】:

  • 现在看起来不错。
  • 我赞成发现最丑陋的 hack 的创造力,而不是因为它是一个可以在实际应用中使用的解决方案 :)
【解决方案3】:

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

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

【讨论】:

  • 如何将 JavaScript 函数作为回调传递给 WKWebView?从what I can see 开始,只允许可序列化的值(字符串、数字、布尔值)。
  • @VidarS.Ramdal,对于那些不能通过值传递的非序列化对象,它们将通过“引用”传递。 “引用”是一个特殊的微小(可序列化)对象,它代表原始的不可序列化对象。应用于引用对象的操作将被转换为 javascript 表达式并由 XWebView 评估。
  • 好吧,那么我想我的问题是你如何操作那个匿名对象。如何从 Swift 调用匿名 JavaScript 函数?
  • 好问题。任何通过引用传递的对象(无论是否匿名)都将是 retained 在启动的插件对象的命名空间下。它将是released,而它的引用是released on native side
【解决方案4】:

可以通过轮询当前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。

【讨论】:

    【解决方案5】:

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

    您通过 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。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-05-21
      • 1970-01-01
      • 2015-04-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多