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:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user