§7.5 路径回退到 writeToFile:撤销 evaluateJavaScript 路径
CLAUDE.md 原则 A 升级为第一准则(H5 端零修改不可妥协)后,重新评估 上一轮 commit246f215引入的 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 头部:标题 / 简介改回写文件路径;中间走过两次弯路的完整决策 链记录在头部(commitf68b0db→246f215→ 本次 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:
co-authored by
Claude Opus 4.7
parent
3039daf903
commit
13cc24b463
@@ -766,7 +766,9 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
|
|||||||
| 2026-06-22 | Design 蓝图按 Contract 全 51 项接口补完实现骨架(H5↔Native 5 条通讯路径):22 项 §3.1 异步 handler / 15 项 §3.2 反向 callback / 3 项 §3.4 弹层 polyfill / 15 项 §7.5 `app_*.js` 预注入全局变量 / 2 项 §3.7 WKUIDelegate alert/confirm | `92298b6` / `c6221d3` / `d48ecc0` / `a06a18c` / `5f5f6a9` / `33574a1` / `f68b0db` |
|
| 2026-06-22 | Design 蓝图按 Contract 全 51 项接口补完实现骨架(H5↔Native 5 条通讯路径):22 项 §3.1 异步 handler / 15 项 §3.2 反向 callback / 3 项 §3.4 弹层 polyfill / 15 项 §7.5 `app_*.js` 预注入全局变量 / 2 项 §3.7 WKUIDelegate alert/confirm | `92298b6` / `c6221d3` / `d48ecc0` / `a06a18c` / `5f5f6a9` / `33574a1` / `f68b0db` |
|
||||||
| 2026-06-22 | Contract §4.2 按 daoqi 原项目代码(grep `var app_` 字面字符串)全面修订:删 `app_gameid` / `app_compareCode`(原项目不写)/ 修正 `app_battery` → `app_getbattery`、`app_network` → `app_getnetwork`(文件名不带 get、变量名带 get)/ 补 6 项遗漏(version / Launchtype / getwifisignalLevel / gamename / invitationcode / gamesname) | `47a89aa` |
|
| 2026-06-22 | Contract §4.2 按 daoqi 原项目代码(grep `var app_` 字面字符串)全面修订:删 `app_gameid` / `app_compareCode`(原项目不写)/ 修正 `app_battery` → `app_getbattery`、`app_network` → `app_getnetwork`(文件名不带 get、变量名带 get)/ 补 6 项遗漏(version / Launchtype / getwifisignalLevel / gamename / invitationcode / gamesname) | `47a89aa` |
|
||||||
| 2026-06-22 | CLAUDE.md 新增第 2 个典型案例「H5 与原生通讯接口的真实路径」记录"凭语义猜测会一字之差犯错、必须 grep 字面字符串验证"的教训 | `fae7b3d` |
|
| 2026-06-22 | CLAUDE.md 新增第 2 个典型案例「H5 与原生通讯接口的真实路径」记录"凭语义猜测会一字之差犯错、必须 grep 字面字符串验证"的教训 | `fae7b3d` |
|
||||||
| 2026-06-22 | **§7.5 重大路径调整**:从「原生 `writeToFile` 写 4 个 `app_*.js` 文件」改为「H5 团队 zip 自带 + 原生 `WKUserScript(.atDocumentEnd)` / `evaluateJavaScript` 覆盖 `window.app_xxx` 全局变量」。Design §7.5 全面重写(`AppDataWriter` → `AppDataInjector`),Contract §4.2 同步新契约,§10 验收清单调整 | 本次 commit |
|
| 2026-06-22 | **§7.5 重大路径调整**:从「原生 `writeToFile` 写 4 个 `app_*.js` 文件」改为「H5 团队 zip 自带 + 原生 `WKUserScript(.atDocumentEnd)` / `evaluateJavaScript` 覆盖 `window.app_xxx` 全局变量」 | `246f215` |
|
||||||
|
| 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 全面同步回退 | 本次 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「典型案例」段。
|
||||||
|
|
||||||
|
|||||||
+24
-32
@@ -690,22 +690,16 @@ _webView.scrollView.showsHorizontalScrollIndicator = NO;
|
|||||||
- LocalStorage 由 H5 自管(WKWebView 默认走 WebsiteDataStore)
|
- LocalStorage 由 H5 自管(WKWebView 默认走 WebsiteDataStore)
|
||||||
- 加载方式:**`loadFileURL:fileURL allowingReadAccessToURL:dirURL`**(file://),不走 HTTP
|
- 加载方式:**`loadFileURL:fileURL allowingReadAccessToURL:dirURL`**(file://),不走 HTTP
|
||||||
|
|
||||||
### 4.2 JS 注入(**2026-06-22 重大路径调整:原生不写文件,只覆盖 `window.app_xxx` 全局变量**)
|
### 4.2 JS 注入(最终路径:原生 `writeToFile` 写文件,与 msext 1:1 等价)
|
||||||
|
|
||||||
H5 业务通过 `<script src="app_*.js">` 同步引入 4 个 `.js` 文件,文件内是 `var app_xxx = "<默认占位>"` 形式的全局变量声明。
|
H5 业务通过 `<script src="app_*.js">` 同步引入 4 个 `.js` 文件,文件内是 `var app_xxx = "..."` 形式的全局变量声明。**原生在 `loadFileURL` 之前用 `writeToFile`/ `String.write(to:)` 把这 4 个文件写到 `{gamedir}/{gamestart}/`,H5 启动时 `<script src>` 同步引入直接读到实际值**。
|
||||||
|
|
||||||
**契约调整**(与原 msext 行为差异):
|
> ⚠️ **2026-06-22 路径决策最终落定**(中间曾尝试改为"H5 自带 + 原生 `evaluateJavaScript` 覆盖 `window.app_xxx`"路径,commit `246f215`,已撤销)。撤销原因:`WKUserScript(.atDocumentStart)` 注入会被 H5 自带 `var app_xxx` 声明覆盖回退;`.atDocumentEnd` 又太晚,H5 业务顶层 `<script>` 已经读过默认值。任何"H5 配合改动"的妥协方案违反 CLAUDE.md 原则 A 第一准则。正确解法:回到 msext 老路写文件,时序 100% 等价。详细分析见 CLAUDE.md「典型案例 3」。
|
||||||
|
|
||||||
| 阶段 | msext 旧契约 | 新契约 |
|
原生侧的工作(与原 msext `NewRootVC.initJSdata` 等价):
|
||||||
|---|---|---|
|
1. **`loadFileURL` 之前**用 `String.write(to:)` 写 4 个 `.js` 文件到 `{gamedir}/{gamestart}/` 沙盒目录
|
||||||
| `app_*.js` 文件来源 | 原生 `NewRootVC.initJSdata` 用 `writeToFile:` 每次启动时写入沙盒 | **H5 团队随 `gamehall.zip` 一起打包**(带默认占位值),原生不写 |
|
2. battery / network 变化时**重写**对应 `.js` 文件(H5 下次 reload 读到新值;业务期间的实时变化由 §3.2 [2][3]反向 callback 推送)
|
||||||
| `app_xxx` 全局变量赋值 | H5 引入文件后由文件内的 `var` 声明赋值 | H5 引入文件后默认占位生效;原生用 `WKUserScript(.atDocumentEnd)` + `evaluateJavaScript` 覆盖 `window.app_xxx` 为实际值 |
|
3. 文件内容用单引号包裹字符串(与 msext 字面对齐),数值字段无引号
|
||||||
| 路径变量 (`{gamedir}/{gamestart}/`) | 原生需知道写入路径 | 由 H5 zip 包结构自带,原生不感知 |
|
|
||||||
|
|
||||||
新契约下原生侧的工作:
|
|
||||||
1. 在 `WKWebViewConfiguration` 装配阶段 addUserScript 一段 `(function(){ window.app_version=1; window.app_gameconfig='...'; ... })()`,injectionTime `.atDocumentEnd`
|
|
||||||
2. battery / network 变化时用 `webView.evaluateJavaScript("window.app_getbattery=...")` 单条覆盖
|
|
||||||
3. 不再 `writeToFile`,不再关心文件路径
|
|
||||||
|
|
||||||
**⚠️ 命名约束:文件名 ≠ 变量名**(沿用 msext 历史)
|
**⚠️ 命名约束:文件名 ≠ 变量名**(沿用 msext 历史)
|
||||||
|
|
||||||
@@ -740,42 +734,39 @@ var app_invitationcode = "<tuiguang_id>"; // 推广码 / 邀请码
|
|||||||
> - `app_gamedir`:大厅 = `self.gamedir`(即 BundleConfig.gameDir)/ 子游戏 = `self.gamefilepath`(子游戏目录)
|
> - `app_gamedir`:大厅 = `self.gamedir`(即 BundleConfig.gameDir)/ 子游戏 = `self.gamefilepath`(子游戏目录)
|
||||||
> - `app_gamestart` + `app_gamename`:大厅 = `self.gamestart` / 子游戏 = `self.game_name`(子游戏名)
|
> - `app_gamestart` + `app_gamename`:大厅 = `self.gamestart` / 子游戏 = `self.game_name`(子游戏名)
|
||||||
|
|
||||||
#### `app_battery.js`(H5 zip 包内,含 `app_getbattery` 默认占位)
|
#### `app_battery.js`(写到 `{gamedir}/{gamestart}/app_battery.js`)
|
||||||
|
|
||||||
H5 端默认形式:
|
|
||||||
```js
|
```js
|
||||||
var app_getbattery = 0.85; // ⚠️ 变量名带 get!msext "var app_getbattery=%lf" double 字面
|
var app_getbattery = 0.85; // ⚠️ 变量名带 get!msext "var app_getbattery=%lf" double 字面
|
||||||
```
|
```
|
||||||
|
|
||||||
原生覆盖时机(每次写一行 `window.app_getbattery = N;`):
|
更新时机:
|
||||||
- 首次 `webView didFinish` 后补一次(保证页面就绪即可读到当前电量)
|
- `loadFileURL` 之前补一次(保证 H5 启动就能读到当前电量)
|
||||||
- `UIDevice.batteryLevelDidChangeNotification` 触发
|
- `UIDevice.batteryLevelDidChangeNotification` 触发重写
|
||||||
- `UIApplication.willEnterForegroundNotification`(前台切换补 retroact)
|
- `UIApplication.willEnterForegroundNotification`(前台切换补 retroact)
|
||||||
|
|
||||||
#### `app_network.js`(H5 zip 包内,含 `app_getnetwork` 默认占位)
|
#### `app_network.js`(写到 `{gamedir}/{gamestart}/app_network.js`)
|
||||||
|
|
||||||
H5 端默认形式:
|
|
||||||
```js
|
```js
|
||||||
var app_getnetwork = 2; // ⚠️ 变量名带 get!msext "var app_getnetwork=%d" int 字面:1 无网 / 2 WiFi / 3 蜂窝
|
var app_getnetwork = 2; // ⚠️ 变量名带 get!msext "var app_getnetwork=%d" int 字面:1 无网 / 2 WiFi / 3 蜂窝
|
||||||
```
|
```
|
||||||
|
|
||||||
原生覆盖时机:
|
更新时机:
|
||||||
- 首次 `webView didFinish` 后补一次
|
- `loadFileURL` 之前补一次
|
||||||
- `NWPathMonitor` 状态变化(msext 用 AFNetworkReachability,新外壳用 NWPathMonitor)
|
- `NWPathMonitor` 状态变化(msext 用 AFNetworkReachability,新外壳用 NWPathMonitor)
|
||||||
- 前台切换
|
- 前台切换
|
||||||
|
|
||||||
#### `app_gamesname.js`(H5 zip 包内,含 `app_gamesname` 默认占位)
|
#### `app_gamesname.js`(写到 `{gamedir}/{gamestart}/app_gamesname.js`)
|
||||||
|
|
||||||
H5 端默认形式:
|
|
||||||
```js
|
```js
|
||||||
var app_gamesname = new Array('game1','game2',...); // ⚠️ var 后面两个空格,沿用 msext AppDelegate.m:259 字面
|
var app_gamesname = new Array('game1','game2',...); // ⚠️ var 后面两个空格,沿用 msext AppDelegate.m:259 字面
|
||||||
```
|
```
|
||||||
|
|
||||||
原生覆盖时机(合并进首次 documentEnd UserScript,即阶段 A):
|
更新时机:
|
||||||
- 首次启动覆盖
|
- `AppDelegate.applicationDidFinishLaunching`(首次启动写入)
|
||||||
- 新装一个子游戏后下次进入大厅时覆盖(msext `gameController.m:197` 历史上注释掉了实际写入,仅 `AppDelegate` 首次有效;新外壳每次进入大厅都覆盖一次,更稳)
|
- 子游戏 `gameController.viewInit`(新装一个子游戏后追加;msext `gameController.m:197` 历史上注释掉了实际写入逻辑,仅 `AppDelegate` 首次写入是活的)
|
||||||
|
|
||||||
> ⚠️ **新外壳约定**:H5 团队保证 `gamehall.zip` 内 4 个 `app_*.js` 都存在,并提供合理默认占位(即使原生注入失败,H5 至少能读到默认值不崩)。原生不再保证"loadFileURL 之前文件已存在"——这是 H5 端打包责任。
|
> H5 模板里 `index.html` 通过 `<script src="app_data.js">` 等同步引入这 4 个文件。新外壳必须保证这些文件**在 loadFileURL 之前**就写好,否则 H5 启动时变量未定义直接崩。
|
||||||
|
|
||||||
#### 修订记录
|
#### 修订记录
|
||||||
|
|
||||||
@@ -784,7 +775,8 @@ var app_gamesname = new Array('game1','game2',...); // ⚠️ var 后面两
|
|||||||
| 2026-06-22 | 删 `app_gameid` / `app_compareCode`:原项目 grep `var app_` 在 `NewRootVC.m:1204` / `gameController.m:2160` 字面字符串里没有这两项 | daoqi 原项目代码全面 grep 验证 |
|
| 2026-06-22 | 删 `app_gameid` / `app_compareCode`:原项目 grep `var app_` 在 `NewRootVC.m:1204` / `gameController.m:2160` 字面字符串里没有这两项 | daoqi 原项目代码全面 grep 验证 |
|
||||||
| 2026-06-22 | `app_battery` 改为 `app_getbattery`、`app_network` 改为 `app_getnetwork`:变量名带 `get` 前缀,与文件名(不带 get)不一致 | daoqi `gameController.m:1191/1198`、`NewRootVC.m:1706/1714` |
|
| 2026-06-22 | `app_battery` 改为 `app_getbattery`、`app_network` 改为 `app_getnetwork`:变量名带 `get` 前缀,与文件名(不带 get)不一致 | daoqi `gameController.m:1191/1198`、`NewRootVC.m:1706/1714` |
|
||||||
| 2026-06-22 | 新增 `app_version` / `app_Launchtype` / `app_getwifisignalLevel` / `app_gamename` / `app_invitationcode` / `app_gamesname` 共 6 项 | daoqi `NewRootVC.m:1204`、`gameController.m:2160`、`AppDelegate.m:259` |
|
| 2026-06-22 | 新增 `app_version` / `app_Launchtype` / `app_getwifisignalLevel` / `app_gamename` / `app_invitationcode` / `app_gamesname` 共 6 项 | daoqi `NewRootVC.m:1204`、`gameController.m:2160`、`AppDelegate.m:259` |
|
||||||
| 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 升级为第一准则后的重新评估 |
|
||||||
|
|
||||||
### 4.3 WKUIDelegate(H5 alert / confirm)
|
### 4.3 WKUIDelegate(H5 alert / confirm)
|
||||||
|
|
||||||
@@ -1004,12 +996,12 @@ Library/Caches/
|
|||||||
- [ ] 重装 + 热启动:< 1 秒进入大厅
|
- [ ] 重装 + 热启动:< 1 秒进入大厅
|
||||||
- [ ] 渠道注入:把 `msext/channel/xxx` 目录名改成 `yyy`,重打包后大厅显示的渠道 ID 变为 `yyy`
|
- [ ] 渠道注入:把 `msext/channel/xxx` 目录名改成 `yyy`,重打包后大厅显示的渠道 ID 变为 `yyy`
|
||||||
- [ ] iOS 18 / 19 / 26 上启动不卡死
|
- [ ] iOS 18 / 19 / 26 上启动不卡死
|
||||||
- [ ] **`app_*.js` 4 个文件由 H5 zip 包内自带**:解压 `gamehall.zip` 后 `app_data.js` / `app_battery.js` / `app_network.js` / `app_gamesname.js` 全部存在(H5 团队打包责任,原生不写)
|
- [ ] **`app_*.js` 4 个文件在沙盒**:`{gamedir}/{gamestart}/` 下 `app_data.js` / `app_battery.js` / `app_network.js` / `app_gamesname.js` 全部存在、loadFileURL 前已落盘(`xcrun simctl get_app_container booted <bundle>` + `cat` 验证)
|
||||||
- [ ] **原生 `WKUserScript(.atDocumentEnd)` 覆盖生效**:在 Safari Web Inspector 读 `window.app_channel` 等 13 项启动期变量,必须是**实际渠道值**(来自 `ChannelConfig.plist`),**不是** H5 zip 包里 `app_data.js` 的默认占位值
|
|
||||||
- [ ] **`app_*` 12 + 1 + 1 + 1 = 15 个全局变量命名 100% 正确**(参 §4.2):H5 console 检查 `app_version` / `app_gameconfig` / `app_gamedir` / `app_gamestart` / `app_agent` / `app_appversion` / `app_market` / `app_channel` / `app_Launchtype` / `app_getwifisignalLevel` / `app_gamename` / `app_invitationcode` / `app_getbattery` / `app_getnetwork` / `app_gamesname` 全部 `!== undefined`;尤其大小写 `app_Launchtype`(L 大写)/ `app_getwifisignalLevel`(wifi 小写 + signal/Level 区分)/ `app_getbattery`、`app_getnetwork`(带 get 前缀)必须精确
|
- [ ] **`app_*` 12 + 1 + 1 + 1 = 15 个全局变量命名 100% 正确**(参 §4.2):H5 console 检查 `app_version` / `app_gameconfig` / `app_gamedir` / `app_gamestart` / `app_agent` / `app_appversion` / `app_market` / `app_channel` / `app_Launchtype` / `app_getwifisignalLevel` / `app_gamename` / `app_invitationcode` / `app_getbattery` / `app_getnetwork` / `app_gamesname` 全部 `!== undefined`;尤其大小写 `app_Launchtype`(L 大写)/ `app_getwifisignalLevel`(wifi 小写 + signal/Level 区分)/ `app_getbattery`、`app_getnetwork`(带 get 前缀)必须精确
|
||||||
|
- [ ] **值是实际渠道值而非占位**:H5 console 读 `app_channel` 应为 `ChannelConfig.plist` 的 channel 值(不是 H5 团队 zip 内可能预留的占位字符串)
|
||||||
- [ ] **大厅 vs 子游戏字面差异**:大厅 H5 内 `app_Launchtype === 0`(数字 0,非字符串 `"0"`)、子游戏 H5 内 `app_Launchtype === 1`
|
- [ ] **大厅 vs 子游戏字面差异**:大厅 H5 内 `app_Launchtype === 0`(数字 0,非字符串 `"0"`)、子游戏 H5 内 `app_Launchtype === 1`
|
||||||
- [ ] **不要存在已撤销的变量**:H5 console 检查 `app_gameid` / `app_compareCode` 应为 `undefined`(早期 Contract §4.2 误列,原项目代码字面字符串里不写)
|
- [ ] **不要存在已撤销的变量**:H5 console 检查 `app_gameid` / `app_compareCode` 应为 `undefined`(早期 Contract §4.2 误列,原项目代码字面字符串里不写)
|
||||||
- [ ] **阶段 B 动态覆盖**:模拟器 Features → Battery State 切换电量;Web Inspector 读 `window.app_getbattery` 应实时跟随;同样切换 Network Link Conditioner 验证 `window.app_getnetwork`
|
- [ ] **battery / network 变化重写**:模拟器 Features → Battery State 切换电量;reload 后 H5 读 `app_getbattery` 应跟随;同样切换 Network Link Conditioner 验证 `app_getnetwork`(业务期间实时变化由 §3.2 [2][3]反向 callback 推送,不依赖文件重写)
|
||||||
|
|
||||||
### B. 桥接
|
### B. 桥接
|
||||||
- [ ] H5 调 `accreditlogin` → 微信弹授权页 → 同意后 H5 收到 `sharelogin` 回调,7 字段完整
|
- [ ] H5 调 `accreditlogin` → 微信弹授权页 → 同意后 H5 收到 `sharelogin` 回调,7 字段完整
|
||||||
|
|||||||
@@ -1968,19 +1968,23 @@ for channel in xianliao 360 qq tencent; do
|
|||||||
done
|
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 路径决策最终落定(中间走过两次弯路,复盘见 CLAUDE.md 典型案例 3)**:
|
||||||
|
|
||||||
> ⚠️ **2026-06-22 重大方向调整**:本节早期版本(commit `f68b0db` 起)设计的是"原生用 `String.write(to:)` 把 4 个 `.js` 文件写到沙盒、H5 通过 `<script src>` 引入"路径,与原 msext `NewRootVC.initJSdata` 写法一致。**实际新外壳约定改为**:H5 端预先把 `app_*.js` 打包在 zip 包里(带默认占位值),原生只负责修改 `window.app_xxx` 全局变量的值,**不再写文件**。理由:
|
|
||||||
>
|
>
|
||||||
> 1. **H5 静态资源由 H5 团队管理**,加上版本号 / 缓存策略后比"原生每次启动都重写"更稳;写文件路径在弱 IO 设备上偶发 `<script src>` 在文件写完前就 fetch 到旧内容
|
> 1. **早期 commit `f68b0db`**:设计为"原生 `writeToFile` 写 4 个 `.js` 文件"路径(与原 msext `NewRootVC.initJSdata` 一致)→ **正确方向**
|
||||||
> 2. **覆盖全局变量更简单**:原生只关心 15 个 `window.app_xxx = "value"`,不关心文件结构 / 字符串包裹符 / 转义规则(H5 的默认占位文件自己处理 var/string/number 形式)
|
> 2. **中间 commit `246f215`**:误以为"H5 自带 `app_*.js`,原生用 `WKUserScript(.atDocumentEnd)` + `evaluateJavaScript` 覆盖 `window.app_xxx`"是更现代化的做法 → **错误调整**,撤销
|
||||||
> 3. **与 §3.2 反向 callback 的事件驱动模型一致**:`getBattery / getnetwork / appservice` 已经在用 `bridge.call(name, data)` 推 H5;现在 `app_*` 也走 `evaluateJavaScript` 路径,两条本质都是"运行时 push 给 H5"
|
> 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` 审查得出)
|
#### 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` |
|
| 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 注入时机与机制
|
#### 7.5.2 4 个文件的写入时机与内容
|
||||||
|
|
||||||
**两阶段注入**:
|
```
|
||||||
|
{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)
|
||||||
| **A. 首次覆盖** | `WKWebViewConfiguration` 装配阶段,`loadFileURL` 之前 | `WKUserScript(injectionTime: .atDocumentEnd, forMainFrameOnly: true)` | #1-#13 共 13 项(启动期已知的渠道 + 启动信息 + 已安装子游戏) |
|
{gamedir}/{gamestart}/app_network.js loadFileURL 前一次 + viewWillAppear + NWPathMonitor 变化(#15)
|
||||||
| **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: - 阶段 A:首次覆盖(documentEnd UserScript)
|
|
||||||
//
|
|
||||||
// 在 WKWebViewConfiguration 装配时 addUserScript:H5 自带的 app_*.js
|
|
||||||
// 全部执行完后这段脚本才跑,把 window.app_xxx 改为实际值,不被 var 声明回退。
|
|
||||||
|
|
||||||
public func makeInitialUserScript() -> WKUserScript {
|
|
||||||
let source = makeInitialJSSource()
|
|
||||||
return WKUserScript(source: source,
|
|
||||||
injectionTime: .atDocumentEnd,
|
|
||||||
forMainFrameOnly: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 阶段 B:动态覆盖(evaluateJavaScript)
|
|
||||||
//
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 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 顶部「重大方向调整」说明。
|
> **不要照搬 `WKUserScript` 注入路径**(曾在 commit `246f215` 尝试过,已撤回):
|
||||||
>
|
> - `.atDocumentStart` 注入会被 H5 自带 `app_*.js` 内的 `var app_xxx = "默认值"` 声明覆盖
|
||||||
> 完整骨架代码在 git history 里:`git show 9f4ccb7 -- docs/H5-Native-Implementation-Design.md` 可查到 `AppDataWriter` struct。Phase 2 实施按 §7.5.3 `AppDataInjector` 落地,**不要照搬** AppDataWriter。
|
> - `.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
|
```swift
|
||||||
// Source/WebView/AppDataWriter.swift —— 已撤销,请勿实施
|
// Source/WebView/AppDataWriter.swift
|
||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -2318,17 +2177,16 @@ public struct AppDataWriter {
|
|||||||
private func runBootPipelineSteps() async throws {
|
private func runBootPipelineSteps() async throws {
|
||||||
// ... ensureReady / fetch / resolve / upgrade ...
|
// ... ensureReady / fetch / resolve / upgrade ...
|
||||||
|
|
||||||
let injector = AppDataInjector(
|
let writer = AppDataWriter(
|
||||||
bundleConfig: .shared,
|
bundleConfig: .shared,
|
||||||
resolvedVersion: resolved,
|
resolvedVersion: resolved,
|
||||||
containerRole: .lobby
|
containerRole: .lobby
|
||||||
)
|
)
|
||||||
|
|
||||||
// 阶段 A:在 loadFileURL 前把首次覆盖 UserScript 加到 contentController。
|
// 1. loadFileURL 前一次性写 4 个 app_*.js 文件(覆盖 H5 zip 包内自带的版本)
|
||||||
// 脚本会在 H5 的 app_*.js var 声明执行完后跑(documentEnd),
|
try writer.writeInitial() // app_data.js + app_gamesname.js
|
||||||
// 覆盖 #1-#13 共 13 项启动期已知字段
|
try writer.writeBattery(UIDevice.current.batteryLevel) // app_battery.js
|
||||||
let controller = bridgedWebView.webView.configuration.userContentController
|
try writer.writeNetwork(networkMonitor.currentCode) // app_network.js
|
||||||
controller.addUserScript(injector.makeInitialUserScript())
|
|
||||||
|
|
||||||
splash.update(text: "加载大厅...", progress: nil)
|
splash.update(text: "加载大厅...", progress: nil)
|
||||||
bridgedWebView.webView.loadFileURL(
|
bridgedWebView.webView.loadFileURL(
|
||||||
@@ -2336,35 +2194,18 @@ private func runBootPipelineSteps() async throws {
|
|||||||
allowingReadAccessTo: SandboxPaths.lobbyRoot
|
allowingReadAccessTo: SandboxPaths.lobbyRoot
|
||||||
)
|
)
|
||||||
|
|
||||||
// 阶段 B:H5 加载完后持续监听 battery / network 变化,
|
// 2. 持续监听 battery / network 变化,重写对应 .js 文件
|
||||||
// 用 evaluateJavaScript 单条覆盖 window.app_getbattery / app_getnetwork
|
// (H5 此次加载已读到首次写入的值;下次 reload 才会读到新值,
|
||||||
|
// 业务期间的实时变化由 §3.2 [2][3]反向 callback 推送)
|
||||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
forName: UIDevice.batteryLevelDidChangeNotification,
|
forName: UIDevice.batteryLevelDidChangeNotification,
|
||||||
object: nil, queue: .main
|
object: nil, queue: .main
|
||||||
) { [weak self] _ in
|
) { _ in
|
||||||
guard let self else { return }
|
try? writer.writeBattery(UIDevice.current.batteryLevel)
|
||||||
Task { @MainActor in
|
|
||||||
await injector.injectBattery(UIDevice.current.batteryLevel,
|
|
||||||
into: self.bridgedWebView.webView)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
networkMonitor.stateDidChange = { [weak self] code in
|
networkMonitor.stateDidChange = { code in
|
||||||
guard let self else { return }
|
try? writer.writeNetwork(code)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -2377,93 +2218,99 @@ private func runBootPipelineSteps() async throws {
|
|||||||
|
|
||||||
#### 7.5.5 大厅 / 子游戏 / 弹层差异
|
#### 7.5.5 大厅 / 子游戏 / 弹层差异
|
||||||
|
|
||||||
| 容器 | 阶段 A UserScript 覆盖(13 项) | 阶段 B evaluateJavaScript 动态(2 项) | 备注 |
|
| 容器 | `app_data.js` 写入 | `app_gamesname.js` | `app_battery.js` | `app_network.js` | 备注 |
|
||||||
|------|---|---|---|
|
|------|---|---|---|---|---|
|
||||||
| 大厅(WebContainer.lobby) | `app_Launchtype=0` + `gamedir = BundleConfig.gameDir` + `gamename = gameStart` + 已安装子游戏列表 | `app_getbattery` / `app_getnetwork` 实时变化 | `app_appversion` 用远端 ResolvedVersion |
|
| 大厅(WebContainer.lobby) | `app_Launchtype=0` + `gamedir = BundleConfig.gameDir` + `gamename = gameStart` | 写当前已安装子游戏列表 | ✓ | ✓ | `app_appversion` 用远端 ResolvedVersion |
|
||||||
| 子游戏(WebContainer.subGame) | `app_Launchtype=1` + `gamedir = 子游戏目录` + `gamename = 子游戏名` + 同份子游戏列表 | 同上 | 子游戏专属 `result_state` 字段,新外壳暂用同 appVersion |
|
| 子游戏(WebContainer.subGame) | `app_Launchtype=1` + `gamedir = 子游戏目录` + `gamename = 子游戏名` | 同样写(与大厅同份内容) | ✓ | ✓ | 子游戏专属 `result_state` 字段,新外壳暂用同 appVersion |
|
||||||
| 弹层(OverlayViewController / threeView) | ✗ 不注入 | ✗ | 弹层 H5 走 `window.settings.{backgameData/browser/finishweb}`(§3.4),不读 `app_*` 全局变量 |
|
| 弹层(OverlayViewController / threeView) | ✗ 不写 | ✗ | ✗ | ✗ | 弹层 H5 走 `window.settings.{backgameData/browser/finishweb}`(§3.4),不读 `app_*` 全局变量 |
|
||||||
|
|
||||||
#### 7.5.6 与原 msext 的差异
|
#### 7.5.6 与原 msext 的差异
|
||||||
|
|
||||||
| 维度 | msext 现状 | 新外壳决策 |
|
| 维度 | msext 现状 | 新外壳决策 |
|
||||||
|------|---|---|
|
|------|---|---|
|
||||||
| 注入主体 | `NewRootVC.initJSdata` / `gameController.initJSdata` 散落在 VC 内,**写文件** | 抽象为 `AppDataInjector` struct,三个容器复用同一份代码,**evaluateJavaScript** |
|
| 写入主体 | `NewRootVC.initJSdata` / `gameController.initJSdata` 散落在 VC 内 | 抽象为 `AppDataWriter` struct,三个容器复用同一份代码 |
|
||||||
| 注入机制 | `[NSString writeToFile:atomically:YES encoding:UTF8 error:nil]` 写 `app_*.js` 到沙盒,H5 通过 `<script src>` 同步引入 | H5 端 `app_*.js` 由 H5 团队自带(默认占位值),原生 `WKUserScript(.atDocumentEnd)` + `evaluateJavaScript` 覆盖 `window.app_xxx` 全局变量 |
|
| 写入机制 | `[NSString writeToFile:atomically:YES encoding:UTF8 error:nil]` 写 `app_*.js` 到沙盒 | `String.write(to:atomically:encoding:)`(等价) |
|
||||||
| 数据源 | `self.gameconfig` 等实例属性,跨多处赋值 | 统一从 `BundleConfig.shared` + `ResolvedVersion` 拉,单一真相 |
|
| 数据源 | `self.gameconfig` 等实例属性,跨多处赋值 | 统一从 `BundleConfig.shared` + `ResolvedVersion` 拉,单一真相 |
|
||||||
| battery / network 时机 | `viewWillAppear` + 前台通知双重保险 + 重写文件 | `addObserver` 注册一次 + `evaluateJavaScript` 单条覆盖 + 首次 didFinish 补一次 |
|
| battery / network 时机 | `viewWillAppear` + 前台通知双重保险 | `addObserver` 注册一次 + `loadFileURL` 前补一次首次值(避免双重写入) |
|
||||||
| 字符串包裹符 | `var x='%@'` 单引号,沿用 msext 字面 | `window.x='...'` 单引号一致 |
|
| 字符串包裹符 | `var x='%@'` 单引号 | `var x='...'` 单引号一致(与 msext 字面对齐) |
|
||||||
| 转义处理 | 直接 `stringWithFormat:@"var x='%@'"` 不转义 | 完整转义 4 种特殊字符(避免渠道字段含单引号导致 H5 JS 解析错) |
|
| 转义处理 | 直接 `stringWithFormat:@"var x='%@'"` 不转义 | 完整转义 4 种特殊字符(避免渠道字段含单引号导致 H5 JS 解析错) |
|
||||||
| 大小写 | `app_Launchtype` / `app_getwifisignalLevel` 严格保持 | 同款,注释提示 |
|
| 大小写 | `app_Launchtype` / `app_getwifisignalLevel` 严格保持 | 同款,注释提示 |
|
||||||
|
| 文件位置 | `{gamedir}/{gamestart}/app_*.js`(沙盒 Caches)| 同款(统一走 `SandboxPaths.lobbyAssets` / `subGameAssets`)|
|
||||||
|
|
||||||
#### 7.5.7 与 §3.4.1 撤销 polyfill 的关系
|
#### 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.1 polyfill | ✗ **已撤销** | 仅 iOS<9 旧桥 `window.settings.getXxx()`,本项目最低 iOS 15.6 不需要 |
|
||||||
| §3.4 OverlayBridge polyfill | ✓ **保留** | 弹层 `window.settings.{backgameData/browser/finishweb}`,与系统版本无关 |
|
| §3.4 OverlayBridge polyfill | ✓ **保留** | 弹层 `window.settings.{backgameData/browser/finishweb}`,与系统版本无关 |
|
||||||
| §3.2 事件 callback `getBattery` / `getnetwork` / `appservice` | ✓ **保留** | 业务期 H5 主动收到的变化推送,与 §7.5 `evaluateJavaScript` 覆盖互为补充(推送 + 状态双轨) |
|
| §3.2 事件 callback `getBattery` / `getnetwork` / `appservice` | ✓ **保留** | 业务期 H5 主动收到的变化推送,与 §7.5 文件重写互为补充 |
|
||||||
| msext 原写文件路径 | ✗ **不实施** | §7.5.3.legacy 保留作对照参考 |
|
| `WKUserScript` + `evaluateJavaScript` 覆盖 `window.app_xxx` | ✗ **探索后撤回** | 时序无法 1:1 等价 msext,详细分析见 §7.5 头部 + CLAUDE.md「典型案例 3」|
|
||||||
|
|
||||||
#### 7.5.8 联调时的可观察性(Phase 1.17 调试手段)
|
#### 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 应出现:
|
启动后 Xcode console 应出现:
|
||||||
|
|
||||||
```
|
```
|
||||||
[lobby] makeInitialUserScript (0 games installed):
|
[lobby] write app_data.js → /Users/.../Caches/<gamedir>/<gamestart>/app_data.js
|
||||||
window.app_version = 1
|
app_version = 1
|
||||||
window.app_gameconfig = 'tsgames.daoqi88.cn-config_test-update_jsonv2_test'
|
app_gameconfig = 'tsgames.daoqi88.cn-config_test-update_jsonv2_test'
|
||||||
window.app_gamedir = 'FtJf07...'
|
app_gamedir = 'FtJf07...'
|
||||||
window.app_gamestart = 'gamehall'
|
app_gamestart = 'gamehall'
|
||||||
window.app_agent = 'veRa0qrBf0df...'
|
app_agent = 'veRa0qrBf0df...'
|
||||||
window.app_appversion = '43'
|
app_appversion = '43'
|
||||||
window.app_market = '2'
|
app_market = '2'
|
||||||
window.app_channel = 'FtJf07...'
|
app_channel = 'FtJf07...'
|
||||||
window.app_Launchtype = 0
|
app_Launchtype = 0
|
||||||
window.app_getwifisignalLevel = 1
|
app_getwifisignalLevel = 1
|
||||||
window.app_gamename = 'gamehall'
|
app_gamename = 'gamehall'
|
||||||
window.app_invitationcode = ''
|
app_invitationcode = ''
|
||||||
window.app_gamesname = []
|
[lobby] write app_gamesname.js: 0 games → ...
|
||||||
[lobby] inject battery: app_getbattery=0.85
|
[lobby] write app_battery.js: app_getbattery=0.85 → ...
|
||||||
[lobby] inject network: app_getnetwork=2
|
[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
|
```js
|
||||||
// 阶段 A:13 项启动期覆盖
|
console.log("app_version =", app_version); // 期望 1
|
||||||
console.log("app_version =", window.app_version); // 期望 1
|
console.log("app_channel =", app_channel); // 期望 ChannelConfig.plist channel 值
|
||||||
console.log("app_channel =", window.app_channel); // 期望 ChannelConfig.plist channel 值
|
console.log("app_Launchtype =", app_Launchtype); // 大厅期望 0,⚠️ L 大写
|
||||||
console.log("app_Launchtype =", window.app_Launchtype); // 大厅期望 0,⚠️ L 大写
|
console.log("app_getwifisignalLevel =", app_getwifisignalLevel); // ⚠️ wifi 小写
|
||||||
console.log("app_getwifisignalLevel =", window.app_getwifisignalLevel); // ⚠️ wifi 小写
|
console.log("app_getbattery =", app_getbattery); // ⚠️ 带 get 前缀
|
||||||
console.log("app_invitationcode =", window.app_invitationcode);
|
console.log("app_getnetwork =", app_getnetwork);
|
||||||
console.log("app_gamesname =", window.app_gamesname); // 期望 Array
|
console.log("app_gamesname =", app_gamesname); // 期望 Array
|
||||||
|
|
||||||
// 阶段 B:2 项动态覆盖
|
|
||||||
console.log("app_getbattery =", window.app_getbattery); // ⚠️ 带 get 前缀
|
|
||||||
console.log("app_getnetwork =", window.app_getnetwork);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| 现象 | 可能原因 |
|
| 现象 | 可能原因 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 全部为 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` 驼峰错) |
|
| 某项为 `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()` 实现错 / 子游戏目录路径不对 |
|
| `app_gamesname` 是空数组但实际已装子游戏 | `SandboxPaths.installedSubGameNames()` 实现错 / 子游戏目录路径不对 |
|
||||||
|
|
||||||
**3. 真机调试**:iPhone 上 Safari → 设置 → 高级 → 网页检查器开启,然后 macOS Safari 同样路径。
|
**4. 真机调试**:iPhone 上 Safari → 设置 → 高级 → 网页检查器开启,然后 macOS Safari 同样路径。
|
||||||
|
|
||||||
**4. 阶段 B 动态覆盖手动触发验证**:
|
|
||||||
- battery:模拟器 Features → Battery State → 切换 Charging / Unplugged / 调节电量百分比
|
|
||||||
- network:模拟器 Features → Network Link Conditioner → 切换 100% Loss / Wi-Fi / 3G
|
|
||||||
|
|
||||||
切换后 Xcode console 应出现 `inject battery: ...` / `inject network: ...` 日志,同时 H5 端 console.log 再读应反映新值。
|
切换后 Xcode console 应出现 `inject battery: ...` / `inject network: ...` 日志,同时 H5 端 console.log 再读应反映新值。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user