Design §3.4.1:补 window.settings 同步 getter polyfill(9 项含 getothername)

audit 第二轮发现:Contract §附录 A 旧桥 JSExport 共 28 方法,与 §3.1
异步主桥去重后多出 9 项 H5 同步只读 getter。前一轮 audit 只覆盖 §3.1
主表的 22 项异步 handler,这 9 项 getter 在 Design 完全无覆盖:

  - getchannelName / getmarketname / getOther / getothername(name)
  - getcompareCode / getbattery / getnetwork
  - getGameinstall(name) / getGameplay(jsondata)

这些接口 H5 端形式是同步表达式(var ch = window.settings.getchannelName()),
WKWebView 无法同步返回 JS 值。唯一不破契约(H5 端零修改)的路径:
documentStart 注入数据快照 + 纯 JS getter polyfill,本地查询零延迟。

§3.4.1 新增覆盖:
  - 9 项 getter 业务语义 + 数据源映射表(驳到 BundleConfig / DeviceKit /
    SandboxPaths.installedSubGames)
  - 数据快照时机:静态字段(11 项渠道注入)启动期一次;动态字段
    (battery / network / installedGames)每次 loadFileURL 前重新快照
  - SettingsBridgePolyfill.makeUserScript 完整 JS 注入骨架
  - WebContainerViewController 接入点(runBootPipelineSteps 末尾、
    loadFileURL 前调 installSettingsPolyfill)
  - 与原 msext JSExport 5 维度差异表

待 Phase 2 实施时查清:getcompareCode 在 msext RootVC.m:1560 附近的真实
返回值语义(业务校验码,可能与 zip 版本号或固定值相关)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 07:24:01 +08:00
co-authored by Claude Opus 4.7
parent 92298b657e
commit 82bad8a85d
+166
View File
@@ -489,6 +489,172 @@ public enum OverlayBridge {
3 个 `WKScriptMessageHandler` 在 OverlayViewController 内注册即可。 3 个 `WKScriptMessageHandler` 在 OverlayViewController 内注册即可。
#### 3.4.1 大厅 / 子游戏的 window.settings 同步 getter polyfill(契约 §附录 A 9 项 getter
**背景**:契约 §附录 A 列出 28 项旧桥 JSExport 方法(iOS<9 路径),与 §3.1 异步 callback 主表去重后多出 **9 项 H5 同步只读 getter**
| getter | 返回类型 | 业务语义 | 数据源 |
|---|---|---|---|
| `getchannelName()` | string | 渠道 ID11 项渠道注入之一)| `BundleConfig.shared.channel` |
| `getmarketname()` | string | 市场 ID11 项渠道注入之一)| `BundleConfig.shared.market` |
| `getOther()` | string | 渠道 `other` 字段 | `BundleConfig.shared.other` |
| `getothername(name)` | string | 按 H5 传入 key 动态读 11 项渠道注入任意字段 | `BundleConfig.shared.value(forKey: name)` |
| `getcompareCode()` | int | 业务校验码(msext `RootVC.m` 沿用 zip 版本号或固定值) | 待原 msext 取值确认(Phase 2 实施时查 `RootVC.m:1560` 附近 `getcompareCode` 真实返回值,并对齐) |
| `getbattery()` | double | 当前电池电量 0.01.0 | `UIDevice.current.batteryLevel`(启动期 snapshot 一次) |
| `getnetwork()` | int | 当前网络类型(0 无 / 1 WiFi / 2 蜂窝) | `NWPathMonitor` 当前 pathloadFileURL 前 snapshot |
| `getGameinstall(name)` | int | 子游戏目录是否存在(0/1) | 扫 `SandboxPaths.subGameRoot(name)` 后注入已安装列表 |
| `getGameplay(jsondata)` | void | 契约 §3.1 19**空实现**,H5 仍会调,原生 noop 即可 | — |
**为什么不走 BridgeBus 异步 callback**H5 端代码形式是 `var ch = window.settings.getchannelName()``var b = window.settings.getbattery()` 等**同步表达式**(取值后立即用于业务判断),WKWebView 时代 native 无法同步返回 JS 值(异步 evaluateJavaScript 改不了 H5 端代码 → 违反契约原则 A)。唯一不破契约的实现路径:**WebView 加载前在 `documentStart` 注入完整数据快照 + 同步 JS getter polyfill**,本地查询零延迟。
**数据快照时机**
- 静态字段(渠道 / market / other / appVersion 等 11 项渠道注入):app 启动期读 `ChannelConfig.plist` 后即不变,全程一次即可
- 动态字段(getbattery / getnetwork):**每次 `loadFileURL` 前重新 snapshot 注入**(精度足够,原 msext 自身也只在 `viewDidLoad` 取一次,H5 业务里"启动时刻电量值"被复用整个会话)
- 已安装子游戏列表(getGameinstall):每次 loadFileURL 前扫描沙盒 + 注入 → SwitchOverGameData 解压新子游戏后自然在下次 loadFileURL 刷新
**实现骨架**
```swift
// Source/WebView/SettingsBridgePolyfill.swift
//
// § A 9 H5 getter documentStart JS polyfill
// msext JSExport
//
//
// WebContainerViewController.loadFileURL(lobbyIndex)
// SettingsBridgePolyfill.makeUserScript(BundleConfig + DeviceSnapshot + InstalledGames)
// BridgedWebView WKUserScript WKUserContentController
// loadFileURL H5 window.settings.getXxx()
@MainActor
public enum SettingsBridgePolyfill {
/// documentStart JSdata Native
public static func makeUserScript(snapshot: Snapshot) -> WKUserScript {
let json = (try? JSONSerialization.data(withJSONObject: snapshot.jsonObject))
.flatMap { String(data: $0, encoding: .utf8) }
?? "{}"
let source = """
(function() {
window.__nativeSnapshot = \(json);
window.settings = window.settings || {};
// ── 11 项渠道注入(静态,启动期一次性快照)────────────
window.settings.getchannelName = function() { return window.__nativeSnapshot.channel || ""; };
window.settings.getmarketname = function() { return window.__nativeSnapshot.market || ""; };
window.settings.getOther = function() { return window.__nativeSnapshot.other || ""; };
window.settings.getothername = function(name) {
if (!name) return "";
return (window.__nativeSnapshot.channelConfig || {})[name] || "";
};
// ── 业务校验码 + 设备动态字段(每次 loadFileURL 前刷新)──
window.settings.getcompareCode = function() { return window.__nativeSnapshot.compareCode || 0; };
window.settings.getbattery = function() { return window.__nativeSnapshot.battery || 0.0; };
window.settings.getnetwork = function() { return window.__nativeSnapshot.network || 0; };
// ── 子游戏安装查询(每次 loadFileURL 前快照已安装列表)─
window.settings.getGameinstall = function(name) {
if (!name) return 0;
var list = window.__nativeSnapshot.installedGames || [];
return list.indexOf(name) >= 0 ? 1 : 0;
};
// ── 已知空实现(契约 §3.1 [19],H5 仍会调)────────────
window.settings.getGameplay = function(_jsondata) { /* no-op */ };
})();
"""
return WKUserScript(source: source,
injectionTime: .atDocumentStart,
forMainFrameOnly: true)
}
public struct Snapshot: Sendable {
public let channelConfig: [String: String] // 11
public let channel: String
public let market: String
public let other: String
public let compareCode: Int
public let battery: Double
public let network: Int // 0/1/2
public let installedGames: [String]
public var jsonObject: [String: Any] {
[
"channelConfig": channelConfig,
"channel": channel,
"market": market,
"other": other,
"compareCode": compareCode,
"battery": battery,
"network": network,
"installedGames": installedGames
]
}
/// BundleConfig + DeviceKit + SandboxPaths
@MainActor
public static func make() -> Snapshot {
let bc = BundleConfig.shared
return Snapshot(
channelConfig: bc.asDictionary,
channel: bc.channel,
market: bc.market,
other: bc.other,
compareCode: CompareCodeProvider.current(), //
battery: DeviceKit.batteryLevel(),
network: NetworkMonitor.shared.currentTypeCode,
installedGames: SandboxPaths.installedSubGames()
)
}
}
}
```
**`WebContainerViewController` 接入点**
```swift
private func runBootPipelineSteps() async throws {
// ... ensureReady / fetch / resolve / upgrade ...
// 6. loadFileURL settings polyfill
splash.update(text: "加载大厅...", progress: nil)
let snapshot = SettingsBridgePolyfill.Snapshot.make()
bridgedWebView.installSettingsPolyfill(snapshot: snapshot)
bridgedWebView.webView.loadFileURL(
SandboxPaths.lobbyIndex,
allowingReadAccessTo: SandboxPaths.lobbyRoot
)
}
// Source/WebView/BridgedWebView.swift
extension BridgedWebView {
/// contentController polyfill UserScript
/// loadFileURL H5 documentStart 访 window.settings.getXxx()
public func installSettingsPolyfill(snapshot: SettingsBridgePolyfill.Snapshot) {
let controller = webView.configuration.userContentController
// WebViewJavascriptBridge.js atDocumentStart UserScript init
// settings polyfill
controller.addUserScript(SettingsBridgePolyfill.makeUserScript(snapshot: snapshot))
}
}
```
**与原 msext 的差异**
| 维度 | msext 现状 | 新外壳决策 |
|------|----------|---------|
| JS↔Native 桥 | iOS 9+ JSContext + JSExport(同步原生返回值) | WKUserScript 注入 + 纯 JS polyfill(同步本地返回) |
| 数据传递 | JS 每次调用 selector → 进 ObjC runtime → 返回 | 启动期一次性 snapshot 注入 JS 全局,业务期纯 JS 查询 |
| 渠道注入读取 | `[FuncPublic getFilePath:@"other" PathType:3]` 扫目录 | 直接读 `BundleConfig.shared.channelConfig` 字典 |
| 性能 | 每次 H5 调用都有 JSContext 跨域开销(µs 级) | 业务期纯 JS 查询(ns 级) |
| 跨容器一致性 | 大厅 / 子游戏 / 弹层各自暴露 selector,要同步维护 | 同一份 `SettingsBridgePolyfill` 多处复用,单点定义 |
**注意事项**
- `compareCode` 业务语义需对照 msext `RootVC.m:1560` 附近 `getcompareCode` 真实返回逻辑(Phase 2 实施前查清);当前 Design 暂列字段,实现时补真值
- 验收:契约 §10 验收清单里所有"H5 同步取渠道值 / 设备状态"的项目(如 `window.settings.getchannelName() === channel注入值`)通过即视为契约等价
- 子游戏容器(SubGame WebContainer)也走同一份 polyfill,仅 `installedGames` 字段在子游戏内意义不同(一般不会再调 getGameinstall
### 3.5 性能优化 ### 3.5 性能优化
- **ProcessPool 复用**:大厅、子游戏、弹层共用同一个 `WKProcessPool`,Cookie/Cache 共享,避免重复初始化(800 ms → 50 ms) - **ProcessPool 复用**:大厅、子游戏、弹层共用同一个 `WKProcessPool`,Cookie/Cache 共享,避免重复初始化(800 ms → 50 ms)