§7.5 路径回退到 writeToFile:撤销 evaluateJavaScript 路径

CLAUDE.md 原则 A 升级为第一准则(H5 端零修改不可妥协)后,重新评估
上一轮 commit 246f215 引入的 evaluateJavaScript 路径:

  - WKUserScript(.atDocumentStart) 注入会被 H5 自带 var app_xxx 声明
    覆盖回退
  - WKUserScript(.atDocumentEnd) 又太晚,H5 业务顶层 <script> 已经
    读过默认值
  - 没有"在 <script src> 之后、业务 <script> 之前"的精确时机
  - 任何"让 H5 改一行 / 改一个文件"的妥协方案违反原则 A 第一准则
  - 详细分析见 CLAUDE.md「典型案例 3」

撤销方案:回到原 msext writeToFile 路径,原生在 loadFileURL 之前写
4 个 app_*.js 文件到沙盒,H5 启动时 <script src> 同步引入即可读到
实际值,时序 100% 等价 msext。

Design §7.5 全面回退(-285 / +126 行):
  - §7.5 头部:标题 / 简介改回写文件路径;中间走过两次弯路的完整决策
    链记录在头部(commit f68b0db246f215 → 本次 commit)
  - §7.5.2 注入时机表 → 4 个文件写入时机表,附"为什么不要照搬
    WKUserScript 注入"警告段
  - §7.5.3 删除 AppDataInjector + .legacy 标注;AppDataWriter 升回主路径
    (保留 logger debug 输出 / msext 字面对齐细节 / 转义 / 大小写)
  - §7.5.4 接入点:writeInitial + writeBattery + writeNetwork 三阶段,
    addObserver 持续监听
  - §7.5.5 大厅 / 子游戏 / 弹层差异表回到 "app_data.js / app_gamesname.js
    / app_battery.js / app_network.js" 维度
  - §7.5.6 与 msext 差异表回到"writeToFile 等价化"对比
  - §7.5.7 与 §3.4.1 polyfill 关系:§7.5 文件预写为主路径;
    WKUserScript + evaluateJavaScript "探索后撤回"
  - §7.5.8 联调路径恢复 cat 文件路径,加 4 种现象诊断表

Contract §4.2 同步:
  - 头部说明改回 "原生 writeToFile 写文件" + 撤销中间 evaluateJavaScript
    路径的复盘段
  - 子节标题回到 "写到 {gamedir}/{gamestart}/app_*.js"
  - 子节"更新时机"段回到 loadFileURL 前 / addObserver / 前台切换
  - 删除"H5 团队 zip 自带 + 默认占位"约定(不再适用,原生重新负责写入)
  - 修订记录新增一行"路径再次调整(最终)"

Contract §10 验收清单同步:
  - "H5 zip 自带 4 个 .js" → "沙盒下 4 个 .js 存在(xcrun simctl + cat 验证)"
  - 删"原生 WKUserScript 覆盖生效"项 → 加"值是实际渠道值而非占位"
  - "阶段 B 动态覆盖" → "battery/network 变化重写文件"

Plan §6.4 里程碑加两行:CLAUDE.md A 升级 + §7.5 最终回退。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 12:40:07 +08:00
co-authored by Claude Opus 4.7
parent 3039daf903
commit 13cc24b463
3 changed files with 127 additions and 286 deletions
+100 -253
View File
@@ -1968,19 +1968,23 @@ for channel in xianliao 360 qq tencent; do
done
```
### 7.5 修改 H5 端 `app_*.js` 全局变量值(evaluateJavaScript 路径
### 7.5 H5 `app_*.js` 预注入文件机制(**最终路径:原生 `writeToFile` 写文件**
**用途**H5 业务代码通过 `<script src="app_data.js">` / `<script src="app_battery.js">` / `<script src="app_network.js">` / `<script src="app_gamesname.js">` 等同步 `<script>` 引入 4 个 `.js` 文件。**这 4 个文件由 H5 团队随 `gamehall.zip` 一起打包提供**,文件内是 `var app_xxx = "<默认占位>"` 形式的变量声明 — 原生**不需要生成这些文件**,只需要在 H5 加载完成后用 `WKUserScript(.atDocumentEnd)` + `evaluateJavaScript` 把 15 个 `window.app_xxx` 全局变量的值改为实际的渠道 / 设备 / 启动信息
**用途**H5 业务代码通过 `<script src="app_data.js">` / `<script src="app_battery.js">` / `<script src="app_network.js">` / `<script src="app_gamesname.js">` 等同步 `<script>` 引入 4 个 `.js` 文件。**原生在 `loadFileURL` 之前用 `String.write(to:)` 把这 4 个文件写到 `{gamedir}/{gamestart}/`**,文件内是 `var app_xxx = "<实际渠道/启动/设备值>"` 形式的变量声明H5 启动时 `<script src>` 同步引入直接读到正确值。**这是 H5 在 iOS 9+ 路径下读渠道 / 设备 / 启动信息的唯一真实路径**(§3.4.1 旧桥同步 getter 已撤销)
**这是 H5 在 iOS 9+ 路径下读渠道 / 设备 / 启动信息的唯一真实路径**(§3.4.1 旧桥同步 getter 已撤销)。
> ⚠️ **2026-06-22 重大方向调整**:本节早期版本(commit `f68b0db` 起)设计的是"原生用 `String.write(to:)` 把 4 个 `.js` 文件写到沙盒、H5 通过 `<script src>` 引入"路径,与原 msext `NewRootVC.initJSdata` 写法一致。**实际新外壳约定改为**:H5 端预先把 `app_*.js` 打包在 zip 包里(带默认占位值),原生只负责修改 `window.app_xxx` 全局变量的值,**不再写文件**。理由:
> ⚠️ **2026-06-22 路径决策最终落定(中间走过两次弯路,复盘见 CLAUDE.md 典型案例 3**
>
> 1. **H5 静态资源由 H5 团队管理**,加上版本号 / 缓存策略后比"原生每次启动都重写"更稳;写文件路径在弱 IO 设备上偶发 `<script src>` 文件写完前就 fetch 到旧内容
> 2. **覆盖全局变量更简单**:原生只关心 15 个 `window.app_xxx = "value"`,不关心文件结构 / 字符串包裹符 / 转义规则(H5 的默认占位文件自己处理 var/string/number 形式)
> 3. **与 §3.2 反向 callback 的事件驱动模型一致**`getBattery / getnetwork / appservice` 已经在用 `bridge.call(name, data)` 推 H5;现在 `app_*` 也走 `evaluateJavaScript` 路径,两条本质都是"运行时 push 给 H5"
> 1. **早期 commit `f68b0db`**:设计为"原生 `writeToFile` 写 4 个 `.js` 文件"路径(与原 msext `NewRootVC.initJSdata` 一致)→ **正确方向**
> 2. **中间 commit `246f215`**:误以为"H5 自带 `app_*.js`,原生用 `WKUserScript(.atDocumentEnd)` + `evaluateJavaScript` 覆盖 `window.app_xxx`"是更现代化的做法 → **错误调整**,撤销
> 3. **本次回退**:再次回到"原生 `writeToFile` 写文件"路径,与原 msext 行为 1:1 等价
>
> 旧"写文件"骨架保留在 §7.5.3.legacy 作为对照参考,**不要实施**。
> **为什么 `evaluateJavaScript` 路径不可行**CLAUDE.md 典型案例 3 详记):
> - `WKUserScript(.atDocumentStart)` 注入会被 H5 自带 `app_*.js` 内的 `var app_xxx = "默认值"` 声明覆盖回退
> - `WKUserScript(.atDocumentEnd)` 又太晚(H5 业务顶层 `<script>` 在 documentEnd 之前已跑过 `var x = app_xxx`,读到的是默认占位)
> - WKUserScript 没有"在 `<script src="app_data.js">` 之后、业务 `<script>` 之前"的精确时机
> - 任何"让 H5 改一行 / 改一个文件"的妥协方案都违反 CLAUDE.md **原则 A 第一准则**(H5 端零修改不可妥协),立即否决
>
> **正确解法**:回到 msext 老路 — 原生在 `loadFileURL` **之前**写 `app_*.js` 文件,H5 启动时 `<script src>` 同步引入即可读到实际值,时序 100% 等价 msext。哪怕这是"现代 WKWebView 不推荐"的写法,契约优先于现代化(CLAUDE.md 原则 A.2)。
#### 7.5.1 15 个 `app_*` 变量完整映射(按 daoqi/msext `NewRootVC.initJSdata`、`gameController.initJSdata`、`AppDelegate.applicationDidFinishLaunching` 审查得出)
@@ -2002,175 +2006,30 @@ done
| 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` |
#### 7.5.2 入时机与机制
#### 7.5.2 4 个文件的写入时机与内容
**两阶段注入**
| 阶段 | 时机 | 机制 | 覆盖的变量 |
|------|------|------|---------|
| **A. 首次覆盖** | `WKWebViewConfiguration` 装配阶段,`loadFileURL` 之前 | `WKUserScript(injectionTime: .atDocumentEnd, forMainFrameOnly: true)` | #1-#13 共 13 项(启动期已知的渠道 + 启动信息 + 已安装子游戏) |
| **B. 动态覆盖** | battery / network 变化时 / 进入 viewWillAppear | `webView.evaluateJavaScript("window.app_xxx = ...")` | #14 / #15 各自变化时单条 |
**为什么必须用 `.atDocumentEnd` 而不是 `.atDocumentStart`**H5 的 `<script src="app_data.js">``documentStart` 之后被 fetch 执行,文件内的 `var app_xxx = "<default>"` 声明会**覆盖** documentStart 时我们注入的值。只有等 H5 自带的 `app_*.js` 都执行完(即 `documentEnd`)后再注入 `window.app_xxx = "实际值"` 才能稳定生效。
> JS 内 `var x = ...` 在执行时只对当前作用域里**已经声明**的 `var` 做赋值;用 `window.x = ...` 直接修改全局对象的属性,与 `var` 声明语义等价但不会再被后续 `var` 重新声明 — 这是覆盖型注入的关键。
**为什么不直接让 H5 用 `bridge.callHandler('getChannelConfig')` 拉**:原 H5 业务代码是 `var ch = app_channel`(同步表达式),用 callHandler 会变 async**违反契约原则 A**(H5 端零修改)。`evaluateJavaScript` 注入 `window.app_xxx` 后 H5 同步表达式不变 ⇒ 契约不破。
#### 7.5.3 Swift 骨架(`AppDataInjector`evaluateJavaScript 路径)
设计带可观察性:每次注入都在 Debug 模式下把"注入了哪些 key=value + 用哪个机制(UserScript / evaluateJavaScript"打到控制台,便于联调时一眼对账 H5 端读到的值。Release 自动去 `.debug` 级别避免性能开销 + 防泄漏。
```swift
// Source/WebView/AppDataInjector.swift
import WebKit
import os.log
@MainActor
public struct AppDataInjector {
let bundleConfig: BundleConfig
let resolvedVersion: ResolvedVersion? // fallback nil
let containerRole: ContainerRole
public enum ContainerRole: CustomStringConvertible {
case lobby
case subGame(name: String, dir: String)
public var description: String {
switch self {
case .lobby: return "lobby"
case .subGame(let n, _): return "subGame(\(n))"
}
}
}
private static let log = Logger(subsystem: "ylgamehall", category: "AppDataInjector")
// MARK: - AdocumentEnd UserScript
//
// WKWebViewConfiguration addUserScriptH5 app_*.js
// window.app_xxx var 退
public func makeInitialUserScript() -> WKUserScript {
let source = makeInitialJSSource()
return WKUserScript(source: source,
injectionTime: .atDocumentEnd,
forMainFrameOnly: true)
}
// MARK: - BevaluateJavaScript
//
// battery / network window.app_getbattery / app_getnetwork
public func injectBattery(_ level: Float, into webView: WKWebView) async {
let value = String(format: "%.2f", level)
let js = "window.app_getbattery=\(value);"
await Self.evaluate(js, on: webView)
Self.log.debug("[\(containerRole.description, privacy: .public)] inject battery: app_getbattery=\(value, privacy: .public)")
}
public func injectNetwork(_ code: Int, into webView: WKWebView) async {
let js = "window.app_getnetwork=\(code);"
await Self.evaluate(js, on: webView)
Self.log.debug("[\(containerRole.description, privacy: .public)] inject network: app_getnetwork=\(code, privacy: .public)")
}
// MARK: - JS
private func makeInitialJSSource() -> String {
let bc = bundleConfig
let launchtype: Int
let gameName: String
let gameDir: String
let gameStart: String
switch containerRole {
case .lobby:
launchtype = 0
gameName = bc.gameStart
gameDir = bc.gameDir
gameStart = bc.gameStart
case .subGame(let name, let dir):
launchtype = 1
gameName = name
gameDir = dir
gameStart = name
}
let appVersion = resolvedVersion?.appVersion.description ?? bc.appVersion
let installed = SandboxPaths.installedSubGameNames()
let gamesnameJSArray = "new Array(\(installed.map { "'\(escape($0))'" }.joined(separator: ",")))"
// H5 app_*.js
// - version / Launchtype / getwifisignalLevel / appversion / getbattery / getnetwork
// - msext
// window.app_xxx = ... var var 退
let source = """
;(function(){
window.app_version=1;
window.app_gameconfig='\(escape(bc.gameConfig))';
window.app_gamedir='\(escape(gameDir))';
window.app_gamestart='\(escape(gameStart))';
window.app_agent='\(escape(bc.agent))';
window.app_appversion='\(escape(appVersion))';
window.app_market='\(escape(bc.market))';
window.app_channel='\(escape(bc.channel))';
window.app_Launchtype=\(launchtype);
window.app_getwifisignalLevel=1;
window.app_gamename='\(escape(gameName))';
window.app_invitationcode='\(escape(bc.other))';
window.app_gamesname=\(gamesnameJSArray);
})();
"""
// key=value H5 console.log(app_xxx)
Self.log.debug("""
[\(containerRole.description, privacy: .public)] makeInitialUserScript (\(installed.count, privacy: .public) games installed):
window.app_version = 1
window.app_gameconfig = '\(bc.gameConfig, privacy: .public)'
window.app_gamedir = '\(gameDir, privacy: .public)'
window.app_gamestart = '\(gameStart, privacy: .public)'
window.app_agent = '\(bc.agent, privacy: .public)'
window.app_appversion = '\(appVersion, privacy: .public)'
window.app_market = '\(bc.market, privacy: .public)'
window.app_channel = '\(bc.channel, privacy: .public)'
window.app_Launchtype = \(launchtype, privacy: .public)
window.app_getwifisignalLevel = 1
window.app_gamename = '\(gameName, privacy: .public)'
window.app_invitationcode = '\(bc.other, privacy: .public)'
window.app_gamesname = \(installed, privacy: .public)
""")
return source
}
private static func evaluate(_ js: String, on webView: WKWebView) async {
do {
_ = try await webView.evaluateJavaScript(js)
} catch {
log.warning("evaluateJavaScript failed: \(error.localizedDescription, privacy: .public) for js: \(js, privacy: .public)")
}
}
/// JS string / / H5 app_*.js
private func escape(_ s: String) -> String {
s.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
.replacingOccurrences(of: "\n", with: "\\n")
.replacingOccurrences(of: "\r", with: "\\r")
}
}
```
{gamedir}/{gamestart}/app_data.js 一次性,loadFileURL 前(含 #1-#12 共 12 个 var
{gamedir}/{gamestart}/app_gamesname.js 一次性 + 新装子游戏后增量重写(#13)
{gamedir}/{gamestart}/app_battery.js loadFileURL 前一次 + viewWillAppear + 前台 + batteryLevelDidChange#14
{gamedir}/{gamestart}/app_network.js loadFileURL 前一次 + viewWillAppear + NWPathMonitor 变化(#15
```
#### 7.5.3.legacy 早期 `AppDataWriter`(写文件路径,**不要实施**)
**为什么是写文件**(保留与 msext 等价的时序):H5 业务页面用 `<script src="app_data.js">` **同步**引入,必须在 `loadFileURL` 之前文件已存在于沙盒;H5 启动时 `<script>` 直接读到正确值,H5 业务代码顶层 `var x = app_xxx` 也能直接拿到实际值。
> 早期 §7.5.3 设计是"原生用 `String.write(to:)` 把 4 个 `.js` 文件写到沙盒、H5 通过 `<script src>` 引入"路径(与原 msext 一致)。**已撤销**,理由见 §7.5 顶部「重大方向调整」说明。
>
> 完整骨架代码在 git history 里:`git show 9f4ccb7 -- docs/H5-Native-Implementation-Design.md` 可查到 `AppDataWriter` struct。Phase 2 实施按 §7.5.3 `AppDataInjector` 落地,**不要照搬** AppDataWriter。
> **不要照搬 `WKUserScript` 注入路径**(曾在 commit `246f215` 尝试过,已撤回):
> - `.atDocumentStart` 注入会被 H5 自带 `app_*.js` 内的 `var app_xxx = "默认值"` 声明覆盖
> - `.atDocumentEnd` 又太晚,H5 业务顶层 `<script>` 在 documentEnd 之前已读过 `app_xxx`(默认值)
> - 没有"在 `<script src>` 之后、业务 `<script>` 之前"的精确时机
> - 任何要求 H5 配合改动的方案都违反 CLAUDE.md 原则 A 第一准则
> - 详细分析见 CLAUDE.md「典型案例:`app_*` 注入时序问题」
#### 7.5.3 Swift 骨架(`AppDataWriter`,写文件路径)
设计带可观察性:每次写文件都在 Debug 模式下把"写了哪个文件 + 内容预览 + 路径"打到控制台,便于联调时一眼对账 H5 端读到的值。Release 自动去 `.debug` 级别避免性能开销 + 防泄漏。
```swift
// Source/WebView/AppDataWriter.swift
// Source/WebView/AppDataWriter.swift
import os.log
@MainActor
@@ -2318,17 +2177,16 @@ public struct AppDataWriter {
private func runBootPipelineSteps() async throws {
// ... ensureReady / fetch / resolve / upgrade ...
let injector = AppDataInjector(
let writer = AppDataWriter(
bundleConfig: .shared,
resolvedVersion: resolved,
containerRole: .lobby
)
// A loadFileURL UserScript contentController
// H5 app_*.js var documentEnd
// #1-#13 13
let controller = bridgedWebView.webView.configuration.userContentController
controller.addUserScript(injector.makeInitialUserScript())
// 1. loadFileURL 4 app_*.js H5 zip
try writer.writeInitial() // app_data.js + app_gamesname.js
try writer.writeBattery(UIDevice.current.batteryLevel) // app_battery.js
try writer.writeNetwork(networkMonitor.currentCode) // app_network.js
splash.update(text: "加载大厅...", progress: nil)
bridgedWebView.webView.loadFileURL(
@@ -2336,35 +2194,18 @@ private func runBootPipelineSteps() async throws {
allowingReadAccessTo: SandboxPaths.lobbyRoot
)
// BH5 battery / network
// evaluateJavaScript window.app_getbattery / app_getnetwork
// 2. battery / network .js
// H5 reload
// §3.2 23 callback
UIDevice.current.isBatteryMonitoringEnabled = true
NotificationCenter.default.addObserver(
forName: UIDevice.batteryLevelDidChangeNotification,
object: nil, queue: .main
) { [weak self] _ in
guard let self else { return }
Task { @MainActor in
await injector.injectBattery(UIDevice.current.batteryLevel,
into: self.bridgedWebView.webView)
}
) { _ in
try? writer.writeBattery(UIDevice.current.batteryLevel)
}
networkMonitor.stateDidChange = { [weak self] code in
guard let self else { return }
Task { @MainActor in
await injector.injectNetwork(code, into: self.bridgedWebView.webView)
}
}
// didFinish H5 battery / network
bridgedWebView.didFinishOnce = { [weak self] in
guard let self else { return }
Task { @MainActor in
await injector.injectBattery(UIDevice.current.batteryLevel,
into: self.bridgedWebView.webView)
await injector.injectNetwork(self.networkMonitor.currentCode,
into: self.bridgedWebView.webView)
}
networkMonitor.stateDidChange = { code in
try? writer.writeNetwork(code)
}
}
```
@@ -2377,93 +2218,99 @@ private func runBootPipelineSteps() async throws {
#### 7.5.5 大厅 / 子游戏 / 弹层差异
| 容器 | 阶段 A UserScript 覆盖(13 项) | 阶段 B evaluateJavaScript 动态(2 项) | 备注 |
|------|---|---|---|
| 大厅(WebContainer.lobby | `app_Launchtype=0` + `gamedir = BundleConfig.gameDir` + `gamename = gameStart` + 已安装子游戏列表 | `app_getbattery` / `app_getnetwork` 实时变化 | `app_appversion` 用远端 ResolvedVersion |
| 子游戏(WebContainer.subGame | `app_Launchtype=1` + `gamedir = 子游戏目录` + `gamename = 子游戏名` +份子游戏列表 | 同上 | 子游戏专属 `result_state` 字段,新外壳暂用同 appVersion |
| 弹层(OverlayViewController / threeView | ✗ 不注入 | ✗ | 弹层 H5 走 `window.settings.{backgameData/browser/finishweb}`(§3.4),不读 `app_*` 全局变量 |
| 容器 | `app_data.js` 写入 | `app_gamesname.js` | `app_battery.js` | `app_network.js` | 备注 |
|------|---|---|---|---|---|
| 大厅(WebContainer.lobby | `app_Launchtype=0` + `gamedir = BundleConfig.gameDir` + `gamename = gameStart` | 写当前已安装子游戏列表 | ✓ | ✓ | `app_appversion` 用远端 ResolvedVersion |
| 子游戏(WebContainer.subGame | `app_Launchtype=1` + `gamedir = 子游戏目录` + `gamename = 子游戏名` |样写(与大厅同份内容) | ✓ | ✓ | 子游戏专属 `result_state` 字段,新外壳暂用同 appVersion |
| 弹层(OverlayViewController / threeView | ✗ 不写 | ✗ | ✗ | ✗ | 弹层 H5 走 `window.settings.{backgameData/browser/finishweb}`(§3.4),不读 `app_*` 全局变量 |
#### 7.5.6 与原 msext 的差异
| 维度 | msext 现状 | 新外壳决策 |
|------|---|---|
| 入主体 | `NewRootVC.initJSdata` / `gameController.initJSdata` 散落在 VC 内**写文件** | 抽象为 `AppDataInjector` struct,三个容器复用同一份代码**evaluateJavaScript** |
| 入机制 | `[NSString writeToFile:atomically:YES encoding:UTF8 error:nil]``app_*.js` 到沙盒H5 通过 `<script src>` 同步引入 | H5 端 `app_*.js` 由 H5 团队自带(默认占位值),原生 `WKUserScript(.atDocumentEnd)` + `evaluateJavaScript` 覆盖 `window.app_xxx` 全局变量 |
| 入主体 | `NewRootVC.initJSdata` / `gameController.initJSdata` 散落在 VC 内 | 抽象为 `AppDataWriter` struct,三个容器复用同一份代码 |
| 入机制 | `[NSString writeToFile:atomically:YES encoding:UTF8 error:nil]``app_*.js` 到沙盒 | `String.write(to:atomically:encoding:)`(等价) |
| 数据源 | `self.gameconfig` 等实例属性,跨多处赋值 | 统一从 `BundleConfig.shared` + `ResolvedVersion` 拉,单一真相 |
| battery / network 时机 | `viewWillAppear` + 前台通知双重保险 + 重写文件 | `addObserver` 注册一次 + `evaluateJavaScript` 单条覆盖 + 首次 didFinish 补一次 |
| 字符串包裹符 | `var x='%@'` 单引号,沿用 msext 字面 | `window.x='...'` 单引号一致 |
| battery / network 时机 | `viewWillAppear` + 前台通知双重保险 | `addObserver` 注册一次 + `loadFileURL` 前补一次首次值(避免双重写入) |
| 字符串包裹符 | `var x='%@'` 单引号 | `var x='...'` 单引号一致(与 msext 字面对齐) |
| 转义处理 | 直接 `stringWithFormat:@"var x='%@'"` 不转义 | 完整转义 4 种特殊字符(避免渠道字段含单引号导致 H5 JS 解析错) |
| 大小写 | `app_Launchtype` / `app_getwifisignalLevel` 严格保持 | 同款,注释提示 |
| 文件位置 | `{gamedir}/{gamestart}/app_*.js`(沙盒 Caches)| 同款(统一走 `SandboxPaths.lobbyAssets` / `subGameAssets`|
#### 7.5.7 与 §3.4.1 撤销 polyfill 的关系
| 路径 | 状态 | 用途 |
|---|---|---|
| §7.5 `app_*.js` evaluateJavaScript 修改全局变量 | ✓ **主路径** | iOS 9+ 全部 H5 业务读 `app_xxx` 全局变量;H5 自带 `app_*.js` 默认占位,原生覆盖为实际值 |
| §7.5 `app_*.js` 文件预写 | ✓ **主路径** | iOS 9+ 全部 H5 业务读 `app_xxx` 全局变量;原生 `writeToFile``loadFileURL` 前写入沙盒,H5 `<script src>` 同步引入即可(与 msext 1:1 等价)|
| §3.4.1 polyfill | ✗ **已撤销** | 仅 iOS<9 旧桥 `window.settings.getXxx()`,本项目最低 iOS 15.6 不需要 |
| §3.4 OverlayBridge polyfill | ✓ **保留** | 弹层 `window.settings.{backgameData/browser/finishweb}`,与系统版本无关 |
| §3.2 事件 callback `getBattery` / `getnetwork` / `appservice` | ✓ **保留** | 业务期 H5 主动收到的变化推送,与 §7.5 `evaluateJavaScript` 覆盖互为补充(推送 + 状态双轨) |
| msext 原写文件路径 | ✗ **不实施** | §7.5.3.legacy 保留作对照参考 |
| §3.2 事件 callback `getBattery` / `getnetwork` / `appservice` | ✓ **保留** | 业务期 H5 主动收到的变化推送,与 §7.5 文件重写互为补充 |
| `WKUserScript` + `evaluateJavaScript` 覆盖 `window.app_xxx` | ✗ **探索后撤回** | 时序无法 1:1 等价 msext,详细分析见 §7.5 头部 + CLAUDE.md「典型案例 3」|
#### 7.5.8 联调时的可观察性(Phase 1.17 调试手段)
H5 端读到错误值时,第一步是确认"原生注入了什么、H5 端最终读到什么"。**核心对账**:原生注入的 JS 字符串与 H5 端 `window.app_xxx` 的实时值。
H5 端读到错误值时,第一步是确认"原生了什么、H5 读到什么"。四条对账路径:
**1. Xcode 控制台 — 原生入 log(§7.5.3 已埋点)**
**1. Xcode 控制台 — 原生入 log(§7.5.3 已埋点)**
启动后 Xcode console 应出现:
```
[lobby] makeInitialUserScript (0 games installed):
window.app_version = 1
window.app_gameconfig = 'tsgames.daoqi88.cn-config_test-update_jsonv2_test'
window.app_gamedir = 'FtJf07...'
window.app_gamestart = 'gamehall'
window.app_agent = 'veRa0qrBf0df...'
window.app_appversion = '43'
window.app_market = '2'
window.app_channel = 'FtJf07...'
window.app_Launchtype = 0
window.app_getwifisignalLevel = 1
window.app_gamename = 'gamehall'
window.app_invitationcode = ''
window.app_gamesname = []
[lobby] inject battery: app_getbattery=0.85
[lobby] inject network: app_getnetwork=2
[lobby] write app_data.js → /Users/.../Caches/<gamedir>/<gamestart>/app_data.js
app_version = 1
app_gameconfig = 'tsgames.daoqi88.cn-config_test-update_jsonv2_test'
app_gamedir = 'FtJf07...'
app_gamestart = 'gamehall'
app_agent = 'veRa0qrBf0df...'
app_appversion = '43'
app_market = '2'
app_channel = 'FtJf07...'
app_Launchtype = 0
app_getwifisignalLevel = 1
app_gamename = 'gamehall'
app_invitationcode = ''
[lobby] write app_gamesname.js: 0 games → ...
[lobby] write app_battery.js: app_getbattery=0.85 → ...
[lobby] write app_network.js: app_getnetwork=2 → ...
```
若没出现 → AppDataInjector 没接入 / log subsystem 过滤掉了,先按 §7.5.4 接入点检查。
若没出现 → AppDataWriter 没接入 / log subsystem 过滤掉了,先按 §7.5.4 接入点检查。
**2. H5 console 对账 — Safari Web Inspector(核心手段**
**2. 沙盒文件直接读 — `cat`(确认文件落盘正确**
模拟器跑时 macOS Safari → 开发菜单 → Simulator → 当前 H5 页面 → Web Inspector → Console,逐项 console.log 读 `window.app_xxx`
```bash
xcrun simctl get_app_container booted com.skyapp.ylgamehall data
# 拿到容器路径后
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_data.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_battery.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_network.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_gamesname.js
```
文件应包含 `var app_xxx=...;` 字面字符串。若文件不存在 / 为空 → AppDataWriter 没跑到 / 路径错;若文件正确但 H5 读 undefined → H5 端 `<script src>` 路径不对或加载时序问题。
**3. H5 console 对账 — Safari Web Inspector**
模拟器跑时 macOS Safari → 开发菜单 → Simulator → 当前 H5 页面 → Web Inspector → Console
```js
// 阶段 A13 项启动期覆盖
console.log("app_version =", window.app_version); // 期望 1
console.log("app_channel =", window.app_channel); // 期望 ChannelConfig.plist channel 值
console.log("app_Launchtype =", window.app_Launchtype); // 大厅期望 0,⚠️ L 大
console.log("app_getwifisignalLevel =", window.app_getwifisignalLevel); // ⚠️ wifi 小写
console.log("app_invitationcode =", window.app_invitationcode);
console.log("app_gamesname =", window.app_gamesname); // 期望 Array
// 阶段 B2 项动态覆盖
console.log("app_getbattery =", window.app_getbattery); // ⚠️ 带 get 前缀
console.log("app_getnetwork =", window.app_getnetwork);
console.log("app_version =", app_version); // 期望 1
console.log("app_channel =", app_channel); // 期望 ChannelConfig.plist channel 值
console.log("app_Launchtype =", app_Launchtype); // 大厅期望 0,⚠️ L 大写
console.log("app_getwifisignalLevel =", app_getwifisignalLevel); // ⚠️ wifi 小
console.log("app_getbattery =", app_getbattery); // ⚠️ 带 get 前缀
console.log("app_getnetwork =", app_getnetwork);
console.log("app_gamesname =", app_gamesname); // 期望 Array
```
| 现象 | 可能原因 |
|---|---|
| 全部为 H5 默认占位(不是实际渠道值)| AppDataInjector.makeInitialUserScript 没 addUserScript / injectionTime 错(不是 .atDocumentEnd/ H5 自带 `app_*.js` 的 var 声明在我们之后又跑了一次 |
| 全部为 H5 zip 包内默认占位(不是实际渠道值)| AppDataWriter 没在 loadFileURL 前调到 / 写入路径与 H5 `<script src>` 引入路径不一致 / writeAppData 抛错被吞 |
| 某项为 `undefined` | 拼写错误(`app_launchtype` 小写 l / `app_battery` 少 get / `app_getNetwork` 驼峰错) |
| `app_getbattery` / `app_getnetwork` 是默认值不变 | 阶段 B 钩子没接入 / 模拟器 batteryLevel = -1(模拟器无电池硬件)/ NWPathMonitor 未启动 |
| `app_getbattery` = 默认值不变 | BatteryMonitor 没启动 / 模拟器 batteryLevel = -1(模拟器无电池硬件) |
| `app_gamesname` 是空数组但实际已装子游戏 | `SandboxPaths.installedSubGameNames()` 实现错 / 子游戏目录路径不对 |
**3. 真机调试**iPhone 上 Safari → 设置 → 高级 → 网页检查器开启,然后 macOS Safari 同样路径。
**4. 阶段 B 动态覆盖手动触发验证**
- battery:模拟器 Features → Battery State → 切换 Charging / Unplugged / 调节电量百分比
- network:模拟器 Features → Network Link Conditioner → 切换 100% Loss / Wi-Fi / 3G
**4. 真机调试**iPhone 上 Safari → 设置 → 高级 → 网页检查器开启,然后 macOS Safari 同样路径。
切换后 Xcode console 应出现 `inject battery: ...` / `inject network: ...` 日志,同时 H5 端 console.log 再读应反映新值。