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:
co-authored by
Claude Opus 4.7
parent
92298b657e
commit
82bad8a85d
@@ -489,6 +489,172 @@ public enum OverlayBridge {
|
||||
|
||||
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 | 渠道 ID(11 项渠道注入之一)| `BundleConfig.shared.channel` |
|
||||
| `getmarketname()` | string | 市场 ID(11 项渠道注入之一)| `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.0–1.0 | `UIDevice.current.batteryLevel`(启动期 snapshot 一次) |
|
||||
| `getnetwork()` | int | 当前网络类型(0 无 / 1 WiFi / 2 蜂窝) | `NWPathMonitor` 当前 path(loadFileURL 前 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 注入的 JS。data 是 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 性能优化
|
||||
|
||||
- **ProcessPool 复用**:大厅、子游戏、弹层共用同一个 `WKProcessPool`,Cookie/Cache 共享,避免重复初始化(800 ms → 50 ms)
|
||||
|
||||
Reference in New Issue
Block a user