Files
youle_app_ios_v2/ylgamehall/Source/Bridge/BridgeBus.swift
T
joywayer 4a6b1e1631 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 端配套协议即可双向消息打通
2026-06-22 00:11:54 +08:00

141 lines
5.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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)
}
}