From 5f5f6a9ff9d35c76d5bcb7eeab0eab6fe44ca9ba Mon Sep 17 00:00:00 2001 From: joywayer Date: Mon, 22 Jun 2026 07:32:30 +0800 Subject: [PATCH] =?UTF-8?q?Design=20=C2=A73.4.2=EF=BC=9A=E8=A1=A5=20Openur?= =?UTF-8?q?lTitleData=20handler=20=E2=80=94=20=E5=A4=A7=E5=8E=85=E6=89=93?= =?UTF-8?q?=E5=BC=80=20threeView=20=E5=BC=B9=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖 §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 = "" 把业务数据透传到 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 --- docs/H5-Native-Implementation-Design.md | 127 ++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/docs/H5-Native-Implementation-Design.md b/docs/H5-Native-Implementation-Design.md index c795585..33afe6e 100644 --- a/docs/H5-Native-Implementation-Design.md +++ b/docs/H5-Native-Implementation-Design.md @@ -655,6 +655,133 @@ extension BridgedWebView { - 验收:契约 §10 验收清单里所有"H5 同步取渠道值 / 设备状态"的项目(如 `window.settings.getchannelName() === channel注入值`)通过即视为契约等价 - 子游戏容器(SubGame WebContainer)也走同一份 polyfill,仅 `installedGames` 字段在子游戏内意义不同(一般不会再调 getGameinstall) +#### 3.4.2 OpenurlTitleData handler — 大厅打开 threeView 弹层(契约 §3.1 [15]) + +`OpenurlTitleData` 不在弹层内部,而是**大厅** WVJB 桥的异步 handler,作用是**触发**一个新的 OverlayViewController(threeView 等价容器)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 待加载 URL(HTTP) + // 入参 "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 polyfill(backgameData/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 性能优化 - **ProcessPool 复用**:大厅、子游戏、弹层共用同一个 `WKProcessPool`,Cookie/Cache 共享,避免重复初始化(800 ms → 50 ms)