Design §3.4.2:补 OpenurlTitleData handler — 大厅打开 threeView 弹层

覆盖 §3.1 [15]OpenurlTitleData,是大厅 WVJB 桥的异步 handler,作用是
触发新的 OverlayViewController push 到导航栈。补在 §3.4 弹层桥章节内
(与 §3.4 settings polyfill 一脉相承),叫 §3.4.2。

§3.4.2 OpenurlTitleDataHandler:
  - 节流:throttle final class 持 lastOpen Date,3 秒内重复调用被吞掉但
    cb 仍要回(否则 H5 会等)
  - 入参解析:url 必填;"title " 末尾有空格(msext 历史契约硬约束,必须
    obj["title "]);data 透传;orientation 0/1 但本项目固定横屏
  - 调 LobbyCoordinator.pushOverlay 打开 OverlayViewController

OverlayViewController 接入:
  - WKWebViewConfiguration + UserScript 注入:
    1. §3.4 OverlayBridge.polyfill(settings.backgameData/browser/finishweb)
    2. window.app_data = "<escaped data>" 把业务数据透传到 H5
  - 等价于 msext threeView 的 JSContext setObject:forKey:@"settings" 行为

§3.4.3 与 msext 5 维度差异表:UIWebView → WKWebView / "title " 末尾空格
严格保持 / 节流类替代静态变量 / UserScript atDocumentStart 注入避免 race

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 07:32:30 +08:00
co-authored by Claude Opus 4.7
parent a06a18c3aa
commit 5f5f6a9ff9
+127
View File
@@ -655,6 +655,133 @@ extension BridgedWebView {
- 验收:契约 §10 验收清单里所有"H5 同步取渠道值 / 设备状态"的项目(如 `window.settings.getchannelName() === channel注入值`)通过即视为契约等价 - 验收:契约 §10 验收清单里所有"H5 同步取渠道值 / 设备状态"的项目(如 `window.settings.getchannelName() === channel注入值`)通过即视为契约等价
- 子游戏容器(SubGame WebContainer)也走同一份 polyfill,仅 `installedGames` 字段在子游戏内意义不同(一般不会再调 getGameinstall - 子游戏容器(SubGame WebContainer)也走同一份 polyfill,仅 `installedGames` 字段在子游戏内意义不同(一般不会再调 getGameinstall
#### 3.4.2 OpenurlTitleData handler — 大厅打开 threeView 弹层(契约 §3.1 15])
`OpenurlTitleData` 不在弹层内部,而是**大厅** WVJB 桥的异步 handler,作用是**触发**一个新的 OverlayViewControllerthreeView 等价容器)push 到导航栈,让 H5 弹层页面在 OverlayViewController 内运行。OverlayViewController 内部的 H5 才用 §3.4 的 `window.settings.{backgameData/browser/finishweb}` polyfill 通讯。
```swift
// Source/Bridge/Handlers/OpenurlTitleDataHandler.swift
@MainActor
public struct OpenurlTitleDataHandler {
let bridge: BridgeProtocol
let coordinator: LobbyCoordinator // push OverlayViewController
/// 3 first_Time
/// actor handler MainActor
/// last open Date 线
private final class Throttle {
var lastOpen: Date?
}
private let throttle = Throttle()
public func register() {
bridge.register("OpenurlTitleData", handler: handle)
}
// MARK: - 15 OpenurlTitleData
//
// §3.1 15
// url : string URLHTTP
// "title " : string 沿 obj["title "]
// data : string threeView H5
// orientation : int 0= / 1=
// responseCallback : "OpenurlTitleData"
// : 3 first_Time
private func handle(_ data: BridgeData?, _ cb: BridgeCallback?) async {
defer { cb?(.string("OpenurlTitleData")) }
// 1. 3 cb H5
let now = Date()
if let last = throttle.lastOpen, now.timeIntervalSince(last) < 3 {
Log.info("OpenurlTitleData throttled (last open \(String(format: "%.2f", now.timeIntervalSince(last)))s ago)")
return
}
throttle.lastOpen = now
// 2. "title "
guard let obj = data?.asObject,
let urlStr = obj["url"]?.asString,
let url = URL(string: urlStr)
else {
Log.warn("OpenurlTitleData: bad url payload")
return
}
let title = obj["title "]?.asString ?? "" // "title "
let payload = obj["data"]?.asString ?? ""
let orientation = obj["orientation"]?.asInt ?? 1 //
// 3. push OverlayViewController
let request = OverlayRequest(
url: url,
title: title,
data: payload,
orientation: orientation
)
coordinator.pushOverlay(request)
}
}
public struct OverlayRequest: Sendable {
public let url: URL
public let title: String
public let data: String
public let orientation: Int // 0= / 1=沿
}
```
**OverlayViewController 接入**
```swift
extension LobbyCoordinator {
public func pushOverlay(_ request: OverlayRequest) {
let vc = OverlayViewController(request: request)
navigationController?.pushViewController(vc, animated: true)
}
}
@MainActor
public final class OverlayViewController: UIViewController {
private let request: OverlayRequest
private let webView: WKWebView
public init(request: OverlayRequest) {
self.request = request
// WKWebView §3.4 window.settings polyfillbackgameData/browser/finishweb
let cfg = WKWebViewConfiguration()
cfg.userContentController.addUserScript(
WKUserScript(source: OverlayBridge.polyfill,
injectionTime: .atDocumentStart,
forMainFrameOnly: true)
)
// H5 data JS H5 window.app_data
let escaped = request.data
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
cfg.userContentController.addUserScript(
WKUserScript(source: "window.app_data = \"\(escaped)\";",
injectionTime: .atDocumentStart,
forMainFrameOnly: true)
)
self.webView = WKWebView(frame: .zero, configuration: cfg)
super.init(nibName: nil, bundle: nil)
title = request.title.trimmingCharacters(in: .whitespaces)
}
// webView + load(request) + WKScriptMessageHandler 3 polyfill
}
```
#### 3.4.3 与原 msext 的差异
| 维度 | msext 现状 | 新外壳决策 |
|------|----------|----------|
| 弹层 WebView | iOS 8 时代 `UIWebView` + JSExport `Bridgetwo` | `WKWebView` + `WKUserScript` 注入 polyfill(同步 settings.* 等价) |
| `"title "` 末尾空格 | `[dict objectForKey:@"title "]` | `obj["title "]?.asString` 严格保持,注释说明 |
| 节流 first_Time | `static BOOL first_Time` + `NSTimer` 3 秒后重置 | 闭包内 final class 持 `Date` lastOpen,更线程友好 |
| H5 数据透传 | `[js evaluateScript:[NSString stringWithFormat:@"app_data='%@'", data]]` | `WKUserScript` atDocumentStart 注入,避免 race |
| orientation 入参 | 入参 string 转 int | BridgeData.asInt 直接拿;本项目实际固定横屏 |
### 3.5 性能优化 ### 3.5 性能优化
- **ProcessPool 复用**:大厅、子游戏、弹层共用同一个 `WKProcessPool`,Cookie/Cache 共享,避免重复初始化(800 ms → 50 ms) - **ProcessPool 复用**:大厅、子游戏、弹层共用同一个 `WKProcessPool`,Cookie/Cache 共享,避免重复初始化(800 ms → 50 ms)