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
的方式上有所不同。不是直接分配它,而是使用空iframe
的src
属性。
示例 (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的主要内容,如果未能解决你的问题,请参考以下文章