diff --git a/ylgamehall/Source/Bridge/BridgeBus.swift b/ylgamehall/Source/Bridge/BridgeBus.swift new file mode 100644 index 0000000..bd9c57a --- /dev/null +++ b/ylgamehall/Source/Bridge/BridgeBus.swift @@ -0,0 +1,140 @@ +// +// 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) + } +}