JavaScript 同步原生通信到 WKWebView

Posted

技术标签:

【中文标题】JavaScript 同步原生通信到 WKWebView【英文标题】:JavaScript synchronous native communication to 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 上的变量? 【参考方案1】:

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

【讨论】:

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

我发现了一个进行同步通信的 hack,但还没有尝试过:https://***.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。

【讨论】:

以上是关于JavaScript 同步原生通信到 WKWebView的主要内容,如果未能解决你的问题,请参考以下文章

iOS原生与H5交互

Ajax-原生Ajax详解-同步与异步底层

JavaScript使用Ajax实现异步通信

Swift 接收 javascript 消息不起作用

javascript与php使用json进行数据通信

React—Native开发之原生模块向JavaScript发送事件