// // BridgeBus.swift // ylgamehall // // BridgeProtocol 的具体实现 + WKScriptMessageHandler 适配。 // 详见 docs/H5-Native-Implementation-Design.md §3.3。 // import Foundation import WebKit import os.log /// 桥诊断日志。文件级 `let`(Logger 是 Sendable)以便在 evaluateJavaScript /// 完成回调等非 MainActor 上下文里也能直接用。 private let bridgeLog = Logger(subsystem: "ylgamehall", category: "Bridge") /// 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" /// 容器标识(`lobby` / `subGame` / …),仅用于诊断日志区分是哪个 WebView 的桥。 /// 不参与任何契约行为。 public let label: String 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, label: String = "webview") { self.webView = webView self.label = label super.init() controller.add(self, name: Self.scriptMessageHandlerName) } // MARK: - BridgeProtocol public func register(_ name: String, handler: @escaping BridgeHandler) { handlers[name] = handler bridgeLog.debug("[\(self.label, privacy: .public)] register handler '\(name, privacy: .public)'") } 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 } bridgeLog.debug("[\(self.label, privacy: .public)] → H5 callHandler '\(name, privacy: .public)'") 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) bridgeLog.debug("[\(self.label, privacy: .public)] ← H5 call '\(name, privacy: .public)' data=\(String(describing: msg["data"]), privacy: .public)") guard let handler = handlers[name] else { bridgeLog.error("[\(self.label, privacy: .public)] no NATIVE handler registered for '\(name, privacy: .public)'") 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 { bridgeLog.error("[\(self.label, privacy: .public)] sendToJS 丢弃:webView 已释放") return } guard let data = try? JSONSerialization.data(withJSONObject: payload), let json = String(data: data, encoding: .utf8) else { bridgeLog.error("[\(self.label, privacy: .public)] JSON encode failed for payload: \(String(describing: payload), privacy: .public)") return } // base64 包装避免 JSON 内单引号 / 反斜杠扰乱 JS 字符串字面量 let base64 = Data(json.utf8).base64EncodedString() // 返回 'ok' / 'no-bridge' 以便诊断「消息被静默丢弃」(H5 侧 bridge 未就绪) let js = """ (function(){ if (!window.\(Self.jsBridgeName)) { return 'no-bridge'; } \ window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)'); return 'ok'; })() """ let tag = label let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?" webView.evaluateJavaScript(js) { result, error in if let error { bridgeLog.error("[\(tag, privacy: .public)] sendToJS '\(what, privacy: .public)' evaluateJavaScript 失败: \(error.localizedDescription, privacy: .public)") } else if (result as? String) != "ok" { bridgeLog.error("[\(tag, privacy: .public)] sendToJS '\(what, privacy: .public)' 被丢弃:H5 侧 window.\(Self.jsBridgeName) 不存在") } } } }