业务期 app_getbattery / app_getnetwork 改为 evaluateJavaScript 重新赋值

按用户指示调整变化时机制:

  启动期(保持):loadFileURL 之前写 4 个 app_*.js 文件,H5 同步
    <script src> 引入读到实际值

  业务期(调整):变化时**不重写文件**,改为
    webView.evaluateJavaScript("window.app_getbattery=N;") 直接重新赋值
    全局变量。原因:H5 已加载完后 <script src> 不会再 fetch,重写文件
    对当前页面无影响(仅影响下次 reload),改 evaluateJavaScript 即时
    生效 + 无文件 IO 开销。

  与 §3.2 反向 callback 双轨同时执行:
    - evaluateJavaScript 让 H5 业务期同步读 app_xxx 永远拿到最新值
    - bridge.call 让 H5 注册的 callback handler 收到事件主动响应

WebContainerViewController.writeAppDataFiles 改动:
  - BatteryMonitor.onChange:Task @MainActor await
    webView.evaluateJavaScript("window.app_getbattery=N;") + bridge.call
  - NetworkMonitor.onChange:同款 evaluateJavaScript + bridge.call
  - AppLifecycleObserver:appservice 无对应 app_* 全局变量,只走 bridge.call

文档同步:
  - Design §7.5.2 改为「两阶段」表述:启动期写文件 + 业务期 evaluateJavaScript
  - Design §7.5.4 接入点代码示例同步
  - Contract §4.2 app_battery.js / app_network.js 子节更新时机段
  - Contract §4.2 修订记录加一行
  - Plan §6.4 里程碑加一行 + 前两轮 commit hash 补充

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 18:50:53 +08:00
co-authored by Claude Opus 4.7
parent 23b3a1fdad
commit 4986f36967
4 changed files with 81 additions and 38 deletions
+3 -1
View File
@@ -772,7 +772,9 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
| 2026-06-22 | **CLAUDE.md 原则 A 升级为第一准则**:H5 端零修改不可妥协,禁止任何"H5 改一行 / 改一个文件 / 加 polyfill / 调时机"的妥协方案。加典型案例 3 复盘 `app_*` 注入时序问题 | `3039daf` | | 2026-06-22 | **CLAUDE.md 原则 A 升级为第一准则**:H5 端零修改不可妥协,禁止任何"H5 改一行 / 改一个文件 / 加 polyfill / 调时机"的妥协方案。加典型案例 3 复盘 `app_*` 注入时序问题 | `3039daf` |
| 2026-06-22 | **§7.5 路径再次回退(最终)**:撤销 `246f215`,回到 msext 原写文件路径 `AppDataWriter`。原因:WKUserScript 时序(documentStart 被 H5 自带 var 覆盖、documentEnd 又太晚)无法 1:1 等价 msext,任何要求 H5 配合改动的方案违反原则 A 第一准则。Design §7.5 + Contract §4.2 + §10 全面同步回退 | `13cc24b` | | 2026-06-22 | **§7.5 路径再次回退(最终)**:撤销 `246f215`,回到 msext 原写文件路径 `AppDataWriter`。原因:WKUserScript 时序(documentStart 被 H5 自带 var 覆盖、documentEnd 又太晚)无法 1:1 等价 msext,任何要求 H5 配合改动的方案违反原则 A 第一准则。Design §7.5 + Contract §4.2 + §10 全面同步回退 | `13cc24b` |
| 2026-06-22 | **AppDataWriter 代码落地**:新增 `Source/WebView/AppDataWriter.swift` + `Source/Resource/NetworkMonitor.swift`WebContainer.runBootPipelineSteps 在 step 5 后、step 6 loadFileURL 前调 `writeAppDataFiles(resolved:)` 写 4 个 app_*.js 文件 + 挂 battery/network 变化重写监听。BuildProject 通过 | `829cc8f` | | 2026-06-22 | **AppDataWriter 代码落地**:新增 `Source/WebView/AppDataWriter.swift` + `Source/Resource/NetworkMonitor.swift`WebContainer.runBootPipelineSteps 在 step 5 后、step 6 loadFileURL 前调 `writeAppDataFiles(resolved:)` 写 4 个 app_*.js 文件 + 挂 battery/network 变化重写监听。BuildProject 通过 | `829cc8f` |
| 2026-06-22 | **`app_appversion` / `app_gameconfig` 业务语义纠正**grep `daoqi/msext/NewRootVC.m:1190-1208` 发现 `app_appversion` 不是版本号、是审核切换标志(`'0'` 正常 / `'1'` 审核期);`app_gameconfig` 也跟着 result 在 `BundleConfig.gameConfig` / `appleConfig` 切换。AppDataWriter.writeAppData 加 result 计算逻辑;Contract §4.2 + Design §7.5.1 同步纠正;Contract §4.2 修订记录加一行 | 本次 commit | | 2026-06-22 | **`app_appversion` / `app_gameconfig` 业务语义纠正**grep `daoqi/msext/NewRootVC.m:1190-1208` 发现 `app_appversion` 不是版本号、是审核切换标志(`'0'` 正常 / `'1'` 审核期);`app_gameconfig` 也跟着 result 在 `BundleConfig.gameConfig` / `appleConfig` 切换。AppDataWriter.writeAppData 加 result 计算逻辑;Contract §4.2 + Design §7.5.1 同步纠正;Contract §4.2 修订记录加一行 | `0a313af` |
| 2026-06-22 | **Phase 2.A 8 个大厅 handler 落地** + **Phase 2.B 5 项反向 callback 落地** + BridgeData 加 nonisolated 解 Swift 6 actor 隔离 | `d38ff0f` + `23b3a1f` |
| 2026-06-22 | **业务期 `app_getbattery` / `app_getnetwork` 改为 evaluateJavaScript 重新赋值(不重写文件)**:启动期保持写文件,业务期变化时改为 `webView.evaluateJavaScript("window.app_xxx=N;")` 直接重新赋值(H5 已加载完 `<script src>` 不会再 fetch,重写文件无效)。与 §3.2 反向 callback 双轨同时执行。Design §7.5.2 / §7.5.4 + Contract §4.2 同步 | 本次 commit(用户指示) |
> **§3.4.1 撤销说明**:早期 commit `82bad8a` 实现的 `window.settings.getXxx()` polyfill 已撤销 — Contract §附录 A 自身明示「iOS<9 路径,新外壳如最低系统 ≥ iOS 14 可不实现」,本项目最低 iOS 15.6 → polyfill 路径未启用,H5 用 §7.5 的 `app_*.js` 文件路径作为唯一主路径。详细缘由见 Design §3.4.1(撤销说明)和 CLAUDE.md「典型案例」段。 > **§3.4.1 撤销说明**:早期 commit `82bad8a` 实现的 `window.settings.getXxx()` polyfill 已撤销 — Contract §附录 A 自身明示「iOS<9 路径,新外壳如最低系统 ≥ iOS 14 可不实现」,本项目最低 iOS 15.6 → polyfill 路径未启用,H5 用 §7.5 的 `app_*.js` 文件路径作为唯一主路径。详细缘由见 Design §3.4.1(撤销说明)和 CLAUDE.md「典型案例」段。
+10 -8
View File
@@ -766,10 +766,12 @@ var app_invitationcode = '<tuiguang_id>'; // 推广码 / 邀请码
var app_getbattery = 0.85; // ⚠️ 变量名带 getmsext "var app_getbattery=%lf" double 字面 var app_getbattery = 0.85; // ⚠️ 变量名带 getmsext "var app_getbattery=%lf" double 字面
``` ```
更新时机: 更新时机(启动期写文件 + 业务期 evaluateJavaScript 重新赋值,**不重写文件**)
- `loadFileURL` 之前一次(保证 H5 启动就能读到当前电量) - 启动期:`loadFileURL` 之前一次首次值(保证 H5 启动同步读到当前电量)
- `UIDevice.batteryLevelDidChangeNotification` 触发重写 - 业务期:`UIDevice.batteryLevelDidChangeNotification` 触发 → 原生执行
- `UIApplication.willEnterForegroundNotification`(前台切换补 retroact `webView.evaluateJavaScript("window.app_getbattery=N;")` 重新赋值 H5 全局变量
**不重写文件**H5 已加载完,`<script src>` 不会再 fetch,重写文件对当前页面无影响)
- 同时触发 §3.2 2]反向 callback `getBattery` 给注册了 handler 的 H5 业务
#### `app_network.js`(写到 `{gamedir}/{gamestart}/app_network.js` #### `app_network.js`(写到 `{gamedir}/{gamestart}/app_network.js`
@@ -777,10 +779,9 @@ var app_getbattery = 0.85; // ⚠️ 变量名带 getmsext "var app_getbatt
var app_getnetwork = 2; // ⚠️ 变量名带 getmsext "var app_getnetwork=%d" int 字面:1 无网 / 2 WiFi / 3 蜂窝 var app_getnetwork = 2; // ⚠️ 变量名带 getmsext "var app_getnetwork=%d" int 字面:1 无网 / 2 WiFi / 3 蜂窝
``` ```
更新时机: 更新时机(同 app_battery.js
- `loadFileURL` 之前补一次 - 启动期:`loadFileURL` 之前写首次值
- `NWPathMonitor` 状态变化msext 用 AFNetworkReachability,新外壳用 NWPathMonitor - 业务期:`NWPathMonitor` 状态变化`webView.evaluateJavaScript("window.app_getnetwork=N;")` 重新赋值(不重写文件)+ §3.2 [3]反向 callback `getnetwork`
- 前台切换
#### `app_gamesname.js`(写到 `{gamedir}/{gamestart}/app_gamesname.js` #### `app_gamesname.js`(写到 `{gamedir}/{gamestart}/app_gamesname.js`
@@ -804,6 +805,7 @@ var app_gamesname = new Array('game1','game2',...); // ⚠️ var 后面两
| 2026-06-22 | **路径调整**:从"原生 `writeToFile` 写 4 个 `.js` 文件"改为"H5 团队 zip 包自带 + 原生 `WKUserScript(.atDocumentEnd)` / `evaluateJavaScript` 覆盖 `window.app_xxx` 全局变量值" | H5 团队 / 项目方约定 | | 2026-06-22 | **路径调整**:从"原生 `writeToFile` 写 4 个 `.js` 文件"改为"H5 团队 zip 包自带 + 原生 `WKUserScript(.atDocumentEnd)` / `evaluateJavaScript` 覆盖 `window.app_xxx` 全局变量值" | H5 团队 / 项目方约定 |
| 2026-06-22 | **路径再次调整(最终)**:撤销上一行的 evaluateJavaScript 路径,回到原 msext "原生 `writeToFile` 写文件"路径。原因:WKUserScript 时序无法 1:1 等价 msext`.atDocumentStart` 被 H5 自带 var 覆盖,`.atDocumentEnd` 太晚 H5 业务已读默认值),任何要求 H5 配合改动的方案违反 CLAUDE.md 原则 A 第一准则。Design §7.5 + 本节同步回退 | CLAUDE.md 原则 A 升级为第一准则后的重新评估 | | 2026-06-22 | **路径再次调整(最终)**:撤销上一行的 evaluateJavaScript 路径,回到原 msext "原生 `writeToFile` 写文件"路径。原因:WKUserScript 时序无法 1:1 等价 msext`.atDocumentStart` 被 H5 自带 var 覆盖,`.atDocumentEnd` 太晚 H5 业务已读默认值),任何要求 H5 配合改动的方案违反 CLAUDE.md 原则 A 第一准则。Design §7.5 + 本节同步回退 | CLAUDE.md 原则 A 升级为第一准则后的重新评估 |
| 2026-06-22 | **`app_appversion` 业务语义重大纠正**:早期文档把 `app_appversion` 描述为"App 的版本号字符串",实际是 msext `NewRootVC.m:1190-1208` 的审核切换标志(`'0'``'1'`)。当远端 < 本地时切到 `'1'` + `app_gameconfig` 切到 `BundleConfig.appleConfig`(审核期接口);其它情况都是 `'0'` + `gameConfig`(正常业务)。`app_gameconfig` 数据源同步从"硬编码 gameConfig"改为"动态切换 gameConfig/appleConfig"。Design §7.5.1 同步、代码 AppDataWriter.writeAppData 修复 | daoqi `NewRootVC.m:1190-1208` initJSdata 全面 grep 重读 | | 2026-06-22 | **`app_appversion` 业务语义重大纠正**:早期文档把 `app_appversion` 描述为"App 的版本号字符串",实际是 msext `NewRootVC.m:1190-1208` 的审核切换标志(`'0'``'1'`)。当远端 < 本地时切到 `'1'` + `app_gameconfig` 切到 `BundleConfig.appleConfig`(审核期接口);其它情况都是 `'0'` + `gameConfig`(正常业务)。`app_gameconfig` 数据源同步从"硬编码 gameConfig"改为"动态切换 gameConfig/appleConfig"。Design §7.5.1 同步、代码 AppDataWriter.writeAppData 修复 | daoqi `NewRootVC.m:1190-1208` initJSdata 全面 grep 重读 |
| 2026-06-22 | **业务期 `app_getbattery` / `app_getnetwork` 改为 evaluateJavaScript 重新赋值(不重写文件)**:启动期保持写文件(H5 同步 `<script src>` 需要文件存在);业务期变化时不再重写 `app_battery.js` / `app_network.js`H5 已加载完 `<script src>` 不会再 fetch,重写无效),改为 `webView.evaluateJavaScript("window.app_xxx=N;")` 直接重新赋值全局变量。与 §3.2 反向 callback 双轨同时执行 | WebContainerViewController.writeAppDataFiles 优化(用户指示) |
### 4.3 WKUIDelegateH5 alert / confirm ### 4.3 WKUIDelegateH5 alert / confirm
+51 -21
View File
@@ -2006,16 +2006,29 @@ done
| 14 | `app_getbattery` | `String(format: "%.2f", UIDevice.batteryLevel)` | 同 | `UIDevice.current.batteryLevel`,每次 viewWillAppear + battery 变化通知 | `app_battery.js` | | 14 | `app_getbattery` | `String(format: "%.2f", UIDevice.batteryLevel)` | 同 | `UIDevice.current.batteryLevel`,每次 viewWillAppear + battery 变化通知 | `app_battery.js` |
| 15 | `app_getnetwork` | `"1"`/`"2"`/`"3"` | 同 | `NWPathMonitor` 状态映射(1 无网 / 2 WiFi / 3 蜂窝),每次 viewWillAppear + network 变化通知 | `app_network.js` | | 15 | `app_getnetwork` | `"1"`/`"2"`/`"3"` | 同 | `NWPathMonitor` 状态映射(1 无网 / 2 WiFi / 3 蜂窝),每次 viewWillAppear + network 变化通知 | `app_network.js` |
#### 7.5.2 4 个文件的写入时机与内容 #### 7.5.2 写入时机与机制(**两阶段:启动期写文件 + 业务期 evaluateJavaScript**
``` ```
{gamedir}/{gamestart}/app_data.js 一次性,loadFileURL 前(含 #1-#12 共 12 个 var 启动期(loadFileURL 之前,原生写文件到沙盒,供 H5 同步 <script src> 引入):
{gamedir}/{gamestart}/app_gamesname.js 一次性 + 新装子游戏后增量重写(#13 {gamedir}/{gamestart}/app_data.js 一次性写(#1-#12 共 12 个 var
{gamedir}/{gamestart}/app_battery.js loadFileURL 前一次 + viewWillAppear + 前台 + batteryLevelDidChange#14 {gamedir}/{gamestart}/app_gamesname.js 一次性写#13
{gamedir}/{gamestart}/app_network.js loadFileURL 前一次 + viewWillAppear + NWPathMonitor 变化(#15 {gamedir}/{gamestart}/app_battery.js loadFileURL 前一次(首次值
{gamedir}/{gamestart}/app_network.js loadFileURL 前补一次(首次值)
业务期(H5 已加载完后,原生 evaluateJavaScript 直接重新赋值 window.app_*):
battery 变化 → webView.evaluateJavaScript("window.app_getbattery=N;") (不重写文件)
network 变化 → webView.evaluateJavaScript("window.app_getnetwork=N;") (不重写文件)
子游戏装新 → 重新进入大厅时调 writer.writeGamesName 一次(增量更新文件)
``` ```
**为什么写文件**(保留与 msext 等价的时序):H5 业务页面`<script src="app_data.js">` **同步**引入,必须在 `loadFileURL` 之前文件已存在于沙盒;H5 启动时 `<script>` 直接读到正确值,H5 业务代码顶层 `var x = app_xxx` 也能直接拿到实际值。 **为什么启动期写文件**(保留与 msext 等价的时序):H5 启动时`<script src="app_data.js">` **同步**引入,必须在 `loadFileURL` 之前文件已存在于沙盒;H5 业务代码顶层 `var x = app_xxx` 直接拿到实际值。
**为什么业务期不重写文件,改用 evaluateJavaScript**H5 加载完成后,`<script src>` 不会再 fetch,所以重写文件对当前页面无影响(仅影响下次 reload)。直接 `evaluateJavaScript("window.app_xxx = N;")` 重新赋值全局变量,H5 业务期同步读 `app_xxx` 立即拿到最新值,效率更高、无文件 IO 开销。
> **与 §3.2 反向 callback 的关系**:两条路径并存、互为补充:
> - **`window.app_xxx` 重新赋值**H5 业务代码任何时刻同步读 `app_getbattery` 都拿到最新值
> - **`bridge.call("getBattery", ...)` 反向推送**H5 注册的 callback 收到事件主动响应(业务期变化感知)
> 两路同时执行(onChange 闭包内一次完成),与原 msext changebattery + js eval 双轨等价。
> **不要照搬 `WKUserScript` 注入路径**(曾在 commit `246f215` 尝试过,已撤回): > **不要照搬 `WKUserScript` 注入路径**(曾在 commit `246f215` 尝试过,已撤回):
> - `.atDocumentStart` 注入会被 H5 自带 `app_*.js` 内的 `var app_xxx = "默认值"` 声明覆盖 > - `.atDocumentStart` 注入会被 H5 自带 `app_*.js` 内的 `var app_xxx = "默认值"` 声明覆盖
@@ -2183,10 +2196,15 @@ private func runBootPipelineSteps() async throws {
containerRole: .lobby containerRole: .lobby
) )
// 1. loadFileURL 4 app_*.js H5 zip // 1. loadFileURL 4 app_*.js
try writer.writeInitial() // app_data.js + app_gamesname.js // H5 <script src>
try writer.writeBattery(UIDevice.current.batteryLevel) // app_battery.js NetworkMonitor.shared.start()
try writer.writeNetwork(networkMonitor.currentCode) // app_network.js BatteryMonitor.shared.start()
AppLifecycleObserver.shared.start()
try writer.writeInitial() // app_data.js + app_gamesname.js
try writer.writeBattery(BatteryMonitor.shared.currentLevel) // app_battery.js
try writer.writeNetwork(NetworkMonitor.shared.currentCode) // app_network.js
splash.update(text: "加载大厅...", progress: nil) splash.update(text: "加载大厅...", progress: nil)
bridgedWebView.webView.loadFileURL( bridgedWebView.webView.loadFileURL(
@@ -2194,18 +2212,30 @@ private func runBootPipelineSteps() async throws {
allowingReadAccessTo: SandboxPaths.lobbyRoot allowingReadAccessTo: SandboxPaths.lobbyRoot
) )
// 2. battery / network .js // 2. **** evaluateJavaScript window.app_*
// H5 reload // + bridge.call
// §3.2 23 callback let bridge = bridgedWebView.bridge
UIDevice.current.isBatteryMonitoringEnabled = true let webView = bridgedWebView.webView
NotificationCenter.default.addObserver(
forName: UIDevice.batteryLevelDidChangeNotification, BatteryMonitor.shared.onChange = { level in
object: nil, queue: .main let value = String(format: "%.2f", level)
) { _ in Task { @MainActor in
try? writer.writeBattery(UIDevice.current.batteryLevel) _ = try? await webView.evaluateJavaScript("window.app_getbattery=\(value);")
}
bridge.call("getBattery", data: .string(value), callback: nil)
} }
networkMonitor.stateDidChange = { code in NetworkMonitor.shared.onChange = { code in
try? writer.writeNetwork(code) Task { @MainActor in
_ = try? await webView.evaluateJavaScript("window.app_getnetwork=\(code);")
}
bridge.call("getnetwork", data: .string("\(code)"), callback: nil)
}
// appservice app_* bridge.call
AppLifecycleObserver.shared.onBackground = {
bridge.call("appservice", data: .string("1"), callback: nil)
}
AppLifecycleObserver.shared.onForeground = {
bridge.call("appservice", data: .string("2"), callback: nil)
} }
} }
``` ```
@@ -238,27 +238,36 @@ public final class WebContainerViewController: UIViewController {
try writer.writeNetwork(NetworkMonitor.shared.currentCode) try writer.writeNetwork(NetworkMonitor.shared.currentCode)
let bridge = bridgedWebView.bridge let bridge = bridgedWebView.bridge
let webView = bridgedWebView.webView
// **** evaluateJavaScript
// window.app_* H5 <script src> fetch
// H5 app_xxx
// §3.2 callback handler H5
// docs/H5-Native-Implementation-Design.md §7.5.2 / §7.5.4
// §3.2 2 getBattery UIDevice // §3.2 2 getBattery UIDevice
// data: "%.2f" // data: "%.2f"
BatteryMonitor.shared.onChange = { level in BatteryMonitor.shared.onChange = { level in
try? writer.writeBattery(level) let value = String(format: "%.2f", level)
bridge.call("getBattery", Task { @MainActor in
data: .string(String(format: "%.2f", level)), _ = try? await webView.evaluateJavaScript("window.app_getbattery=\(value);")
callback: nil) }
bridge.call("getBattery", data: .string(value), callback: nil)
} }
// §3.2 3 getnetwork NWPath // §3.2 3 getnetwork NWPath
// data: "1"= / "2"=WiFi / "3"= // data: "1"= / "2"=WiFi / "3"=
NetworkMonitor.shared.onChange = { code in NetworkMonitor.shared.onChange = { code in
try? writer.writeNetwork(code) Task { @MainActor in
bridge.call("getnetwork", _ = try? await webView.evaluateJavaScript("window.app_getnetwork=\(code);")
data: .string("\(code)"), }
callback: nil) bridge.call("getnetwork", data: .string("\(code)"), callback: nil)
} }
// §3.2 4 appservice App 沿 // §3.2 4 appservice App 沿
// data: "1"= / "2"= // data: "1"= / "2"=
// appservice app_* bridge.call
AppLifecycleObserver.shared.onBackground = { AppLifecycleObserver.shared.onBackground = {
bridge.call("appservice", data: .string("1"), callback: nil) bridge.call("appservice", data: .string("1"), callback: nil)
} }