// RDEPUBWebView+JavaScriptBridge.swift // RDEPUBWebView 的 JS 桥接和导航委托扩展 // 实现 WKNavigationDelegate 处理页面加载生命周期, // 实现 WKScriptMessageHandler 接收 JS 消息(进度/选择/链接/错误/固定版式就绪), // 并将内部链接和外部链接路由到委托方法。 import Foundation import WebKit extension RDEPUBWebView { /// 从 URL 解析内部位置(ss-reader:// 协议的链接) func internalLocation(from url: URL) -> RDEPUBLocation? { let href = url.path.removingPercentEncoding?.trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? "" let fragment = url.fragment guard !href.isEmpty || fragment != nil else { return nil } return RDEPUBLocation(href: href, progression: 0, fragment: fragment) } /// 从 JS 消息体中构建当前位置对象 func currentLocation(from body: [String: Any]) -> RDEPUBLocation { let progression = (body["progression"] as? NSNumber)?.doubleValue ?? 0 let lastProgression = (body["lastProgression"] as? NSNumber)?.doubleValue let fragment = body["fragment"] as? String return RDEPUBLocation(href: currentHref, progression: progression, lastProgression: lastProgression, fragment: fragment) } } extension RDEPUBWebView: WKNavigationDelegate { public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didStart", url: webView.url) } public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didCommit", url: webView.url) } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFinish", url: webView.url) applyPresentation() } public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "processTerminated", url: webView.url) } public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFail", url: webView.url, error: error) } public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFailProvisional", url: webView.url, error: error) } /// 导航策略决策:区分内部链接(ss-reader://)、外部链接(http/https/mailto/tel)和其他导航 public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "decidePolicy", url: navigationAction.request.url) guard navigationAction.navigationType == .linkActivated, let url = navigationAction.request.url else { decisionHandler(.allow) return } if url.scheme == RDEPUBResourceURLSchemeHandler.scheme, let location = internalLocation(from: url) { delegate?.epubWebView(self, didActivateInternalLink: location, fromSpineIndex: currentSpineIndex) decisionHandler(.cancel) return } if let scheme = url.scheme?.lowercased(), ["http", "https", "mailto", "tel"].contains(scheme) { delegate?.epubWebView(self, didActivateExternalLink: url) decisionHandler(.cancel) return } decisionHandler(.allow) } } extension RDEPUBWebView: WKScriptMessageHandler { /// 接收 JS 桥接消息,根据消息类型路由到对应的委托方法 public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { RDEPUBWebViewDebug.logMessage(debugScope, webView: message.webView, name: message.name, body: message.body) switch message.name { case RDEPUBJavaScriptBridgeMessage.progressionChanged.rawValue: guard let body = message.body as? [String: Any] else { return } delegate?.epubWebView(self, didUpdateLocation: currentLocation(from: body), spineIndex: currentSpineIndex) case RDEPUBJavaScriptBridgeMessage.selectionChanged.rawValue: if let body = message.body as? [String: Any], let text = body["text"] as? String, let rangeInfo = body["rangeInfo"] as? String { let location = currentLocation(from: body) let selection = RDEPUBSelection(location: location, text: text, rangeInfo: rangeInfo) delegate?.epubWebView(self, didChangeSelection: selection, spineIndex: currentSpineIndex) } else { delegate?.epubWebView(self, didChangeSelection: nil, spineIndex: currentSpineIndex) } case RDEPUBJavaScriptBridgeMessage.internalLink.rawValue: guard let body = message.body as? [String: Any], let href = body["href"] as? String else { return } let location = RDEPUBLocation(href: href, progression: 0) delegate?.epubWebView(self, didActivateInternalLink: location, fromSpineIndex: currentSpineIndex) case RDEPUBJavaScriptBridgeMessage.externalLink.rawValue: guard let body = message.body as? [String: Any], let urlString = body["url"] as? String, let url = URL(string: urlString) else { return } delegate?.epubWebView(self, didActivateExternalLink: url) case RDEPUBJavaScriptBridgeMessage.javaScriptError.rawValue: delegate?.epubWebView(self, didLogJavaScriptError: "\(message.body)") case RDEPUBJavaScriptBridgeMessage.fixedLayoutReady.rawValue: applySearchDecorationsIfNeeded { self.rendered() } default: break } } }