Phase 1.7:BridgeBus 实现 H5↔Native 消息总线 + WKScriptMessageHandler

桥核心的运行时层:把 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('<base64>')
    - callback 用 nativeCallbackCounter 递增成 objc_cb_<n> 入
      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 端配套协议即可双向消息打通
This commit is contained in:
joywayer
2026-06-22 00:11:54 +08:00
parent f740274d59
commit 4a6b1e1631
+140
View File
@@ -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 arrayWVJB
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]) {
// responseIdJS 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)
}
}