iOS Swift中WKWebView取消导航后无法调用evaluateJavaScript及接收事件
问题背景
在iOS 15.0环境下使用Swift开发React Web应用的壳App,已通过WKUserScript和evaluateJavaScript实现原生与Web层的双向通信,且已在Info.plist中配置WKAppBoundDomains,功能正常。但在WKNavigationDelegate的webView(_:decidePolicyFor:decisionHandler:)中调用decisionHandler(.cancel)阻止非主域名或自定义Scheme(如another-app://)的导航后,出现以下问题:
userContentController(_:didReceive:)无法接收Web层消息evaluateJavaScript抛出WKErrorDomain Code=14错误,提示目标帧不在App绑定域内
代码演示及现象:
// Executing javascript in WebView webView.evaluateJavaScript("window.location.href;") { result, error in print(result, error) } // Cancelling navigation to the external URL print("Cancelling navigation") decisionHandler(.cancel) // 此操作导致WebView状态异常 // Executing javascript in WebView, **just as before** webView.evaluateJavaScript("window.location.href;") { result, error in print(#line, result, error) }
输出:
> Optional("http://my-url.com") > Cancelling navigation > Optional(Error Domain=WKErrorDomain Code=14 "JavaScript execution targeted a frame that is not in an app-bound domain" UserInfo={WKJavaScriptExceptionLineNumber=0, WKJavaScriptExceptionMessage=Unable to execute JavaScript in a frame that is not in an app-bound domain, WKJavaScriptExceptionColumnNumber=0, NSLocalizedDescription=JavaScript execution targeted a frame that is not in an app-bound domain})
问题原因
WKWebView在处理导航请求时,即使调用decisionHandler(.cancel)取消导航,内部仍会将当前帧的上下文切换为目标URL的域。由于目标URL不在WKAppBoundDomains列表中,后续的JS执行和消息通信会被系统限制,导致上述异常。
解决方案
方案1:从Web层拦截导航请求(推荐)
在Web层通过JS拦截外部链接和自定义Scheme的导航操作,直接向原生发送消息处理,避免触发原生的导航事件,从根源上避免WebView进入异常状态。
Web层JS代码:
document.addEventListener('click', (e) => { const linkElement = e.target.closest('a'); if (!linkElement || !linkElement.href) return; const url = new URL(linkElement.href); const allowedDomain = 'my-url.com'; // 拦截非主域名或自定义Scheme的链接 if (url.host !== allowedDomain || url.scheme === 'another-app') { e.preventDefault(); // 发送消息给原生处理 window.webkit.messageHandlers.nativeBridge.postMessage({ action: 'handleExternalURL', url: linkElement.href }); } });
原生Swift代码:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let body = message.body as? [String: Any], let action = body["action"] as? String, let urlStr = body["url"] as? String, let url = URL(string: urlStr) else { return } if action == "handleExternalURL" { if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } }
方案2:原生层拦截后恢复绑定域上下文
如果必须在原生层处理导航拦截,在调用decisionHandler(.cancel)后,立即重新加载原绑定域页面,将WebView上下文恢复到允许JS交互的状态。
Swift代码:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } let mainDomain = "my-url.com" // 处理自定义Scheme if url.scheme == "another-app" { if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } decisionHandler(.cancel) // 恢复原页面上下文 webView.load(URLRequest(url: URL(string: "http://\(mainDomain)")!)) return } // 处理非主域名的HTTP/HTTPS链接 if url.host != mainDomain { decisionHandler(.cancel) // 恢复原页面上下文 webView.load(URLRequest(url: URL(string: "http://\(mainDomain)")!)) return } decisionHandler(.allow) }
方案3:针对自定义Scheme的优化处理
对于自定义Scheme,可直接调用系统打开外部App,无需等待WebView加载请求,确保cancel操作不会影响当前页面上下文:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } if url.scheme == "another-app" { // 优先处理外部App打开 if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } // 立即cancel,避免WebView加载该Scheme decisionHandler(.cancel) return } // 其他导航逻辑 decisionHandler(.allow) }
内容的提问来源于stack exchange,提问作者Rasmus Englund

