From 4a6b1e1631dea4d780b71576ed38284d3a6ca9e8 Mon Sep 17 00:00:00 2001 From: joywayer Date: Mon, 22 Jun 2026 00:11:54 +0800 Subject: [PATCH] =?UTF-8?q?Phase=201.7=EF=BC=9ABridgeBus=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20H5=E2=86=94Native=20=E6=B6=88=E6=81=AF=E6=80=BB?= =?UTF-8?q?=E7=BA=BF=20+=20WKScriptMessageHandler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桥核心的运行时层:把 BridgeProtocol 落到 WKWebView 上,承担 JS → Native 消息分发 + Native → H5 主动调用 + 双向 callback 配对。 - 新增 ylgamehall/Source/Bridge/BridgeBus.swift - @MainActor final class,BridgeProtocol + WKScriptMessageHandler 双重 conform;init(webView:, controller:) 自动向 controller 注册 WVJBHandler 通道 - JS → Native(userContentController:didReceive:) - body 兼容单条 dict 与 batch [[dict]] - 优先识 responseId → 查 pendingCallbacks 派给 Native 端 callback - 否则按 handlerName 查 handlers,构造异步 responseCallback 闭包, handler 是 @Sendable async 用独立 Task 执行 - Native → H5(call(_:data:callback:)) - 拼 payload {handlerName, data, callbackId?},base64 后注入 WebViewJavascriptBridge._handleMessageFromObjC('') - callback 用 nativeCallbackCounter 递增成 objc_cb_ 入 pendingCallbacks,等 JS 响应回收 - sendResponseToJS:拼 {responseId, responseData} 走同一通道 - makeResponseCallback 辅助方法:把多层嵌套闭包拆出来,避免 Swift 6 类型推断在深嵌套 + actor 隔离上 ICE - 资源生命周期:webView 弱引用避免循环;BridgeBus 由 controller.add(self) retains,WebView 释放后 controller 一并清空 handler 列表自然 release - BuildProject 通过,Swift 6 严格并发 0 warning;下一步 Phase 1.8 注入 marcuswestin/WVJB JS 端配套协议即可双向消息打通 --- ylgamehall/Source/Bridge/BridgeBus.swift | 140 +++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 ylgamehall/Source/Bridge/BridgeBus.swift 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) + } +}