// // BridgeBus.swift // ylgamehall // // BridgeProtocol 的具体实现 + WKScriptMessageHandler 适配。 // 详见 docs/H5-Native-Implementation-Design.md §3.3。 // import Foundation import WebKit /// H5 ↔ Native 消息总线。 /// /// **运行时约束**:必须挂在 WKWebView 上、@MainActor 单线程访问。 /// **生命周期**:构造时自动向 `WKUserContentController` 注册 `WVJBHandler` 通道, /// 销毁时由 WebView 链路自然释放(无需手动 remove)。 @MainActor public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler { /// JS → Native 的 WKScriptMessageHandler 通道名。 /// JS 端通过 `window.webkit.messageHandlers.WVJBHandler.postMessage({...})` 发送。 public static let scriptMessageHandlerName = "WVJBHandler" /// JS 端注入的全局桥对象名(约定 `window.WebViewJavascriptBridge`)。 public static let jsBridgeName = "WebViewJavascriptBridge" private weak var webView: WKWebView? private var handlers: [String: BridgeHandler] = [:] private var pendingCallbacks: [String: BridgeCallback] = [:] private var nativeCallbackCounter: UInt64 = 0 /// 构造时绑定 WebView + 用户内容控制器,自动注册 `WVJBHandler` 消息处理器。 public init(webView: WKWebView, controller: WKUserContentController) { self.webView = webView super.init() controller.add(self, name: Self.scriptMessageHandlerName) } // MARK: - BridgeProtocol public func register(_ name: String, handler: @escaping BridgeHandler) { handlers[name] = handler } public func call(_ name: String, data: BridgeData?, callback: BridgeCallback?) { var payload: [String: Any] = ["handlerName": name] if let data { payload["data"] = data.jsonObject } if let callback { nativeCallbackCounter += 1 let cbId = "objc_cb_\(nativeCallbackCounter)" pendingCallbacks[cbId] = callback payload["callbackId"] = cbId } sendToJS(payload: payload) } // MARK: - WKScriptMessageHandler public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { // body 可能是单条 dict 或一批 dict array(WVJB 支持批处理) if let batch = message.body as? [[String: Any]] { for msg in batch { handleIncoming(msg) } } else if let single = message.body as? [String: Any] { handleIncoming(single) } else { print("[BridgeBus] unrecognized message body type: \(type(of: message.body))") } } // MARK: - 私有 private func handleIncoming(_ msg: [String: Any]) { // 优先判 responseId(JS 响应 Native 早前的 callHandler) if let responseId = msg["responseId"] as? String { let respData = msg["responseData"].flatMap { BridgeData(jsonObject: $0) } if let cb = pendingCallbacks.removeValue(forKey: responseId) { cb(respData) } else { print("[BridgeBus] stale responseId: \(responseId)") } return } // 否则是 JS → Native handler 调用 guard let name = msg["handlerName"] as? String else { print("[BridgeBus] message missing handlerName: \(msg)") return } let data = msg["data"].flatMap { BridgeData(jsonObject: $0) } let callbackId = msg["callbackId"] as? String // 构造一个回调闭包,handler 调用即把 responseData 异步派回 JS let responseCallback: BridgeCallback? = makeResponseCallback(for: callbackId) guard let handler = handlers[name] else { print("[BridgeBus] no handler registered for '\(name)'") responseCallback?(nil) return } // handler 是 async,独立 Task 内 await;本方法保持 sync 返回 Task { await handler(data, responseCallback) } } private func makeResponseCallback(for callbackId: String?) -> BridgeCallback? { guard let id = callbackId else { return nil } return { [weak self] respData in Task { @MainActor in self?.sendResponseToJS(responseId: id, data: respData) } } } private func sendResponseToJS(responseId: String, data: BridgeData?) { var payload: [String: Any] = ["responseId": responseId] if let data { payload["responseData"] = data.jsonObject } sendToJS(payload: payload) } private func sendToJS(payload: [String: Any]) { guard let webView else { return } guard let data = try? JSONSerialization.data(withJSONObject: payload), let json = String(data: data, encoding: .utf8) else { print("[BridgeBus] JSON encode failed for payload: \(payload)") return } // base64 包装避免 JSON 内单引号 / 反斜杠扰乱 JS 字符串字面量 let base64 = Data(json.utf8).base64EncodedString() let js = "if (window.\(Self.jsBridgeName)) window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)');" webView.evaluateJavaScript(js, completionHandler: nil) } }