§7.5 重大路径调整:app_* 改为 evaluateJavaScript 覆盖 window.app_xxx
H5 团队新约定:app_*.js 4 个文件由 H5 团队自带在 gamehall.zip 内(含
默认占位值),原生**不再写文件**,只用 WKUserScript(.atDocumentEnd) +
evaluateJavaScript 覆盖 window.app_xxx 全局变量的实际值。
Design 三处大改 (+339/-91 净 +248 行):
§7.5 标题 / 头部:
- 标题:H5 app_*.js 预注入文件机制 → 修改 H5 端 app_*.js 全局变量值
(evaluateJavaScript 路径)
- 新增「2026-06-22 重大方向调整」说明段:3 条理由 + 旧路径降级为
§7.5.3.legacy
§7.5.2 注入时机:
- 旧:4 个文件写入时机表
- 新:两阶段注入表(A 首次 documentEnd UserScript / B 动态
evaluateJavaScript)+ 为什么必须 documentEnd 不能 documentStart
(H5 的 var 声明会回退我们的值)+ 为什么不直接走异步 callHandler
(违反契约原则 A)
§7.5.3 Swift 骨架:
- AppDataWriter (writeToFile) → AppDataInjector (UserScript +
evaluateJavaScript)
- 阶段 A makeInitialUserScript:拼装 (function(){window.app_xxx=...})()
documentEnd 注入;包含 13 项启动期已知字段
- 阶段 B injectBattery / injectNetwork:async evaluateJavaScript 单条覆盖
- Logger.debug 保留所有 key=value 打印(联调可观察性)
- 旧 AppDataWriter 保留为 §7.5.3.legacy(明示不要实施)
§7.5.4 接入点:
- configuration.userContentController.addUserScript(阶段 A)
- addObserver(batteryLevelDidChange) → injectBattery
- networkMonitor.stateDidChange → injectNetwork
- bridgedWebView.didFinishOnce 首次补一次 battery + network
§7.5.5 / .6 / .7 / .8 同步调整:差异表 / msext 对比 / 联调路径
- §7.5.8 路径 2 从「沙盒 cat .js 文件」改为「Safari Web Inspector 看
window.app_xxx 实时值」(不再写文件无文件可 cat)
- 加阶段 B 手动触发验证(模拟器 Battery State / Network Link Conditioner)
Contract §4.2 同步:
- 新增 msext 旧契约 vs 新契约对比表(3 维度:文件来源 / 赋值机制 / 路径感知)
- 子节标题从「app_battery.js(写到 ...)」改为「H5 zip 包内,含
app_getbattery 默认占位」+ 更新时机改写为 evaluateJavaScript 时机
- 「H5 团队保证 zip 内 4 个 app_*.js 存在 + 合理默认占位」约定
- 修订记录加一行路径调整
Contract §10 验收清单调整:
- 文件验收:4 个 .js 由 H5 zip 自带(不是原生生成)
- 新增「原生覆盖生效」验收:Web Inspector 读 window.app_channel 必须
是 ChannelConfig.plist 实际值,不是 H5 默认占位
- app_Launchtype 类型修正为数字 0/1(不是字符串)
- 新增「阶段 B 动态覆盖」验收:模拟器手动触发 battery/network 变化
Plan §6.4 里程碑加新一行记录本次路径调整。
至此 H5↔Native 通讯 5 条路径全部按新契约对齐:
1. WVJB 异步 callback(22 项)
2. Native→H5 反向 callback(15 项)
3. **window.app_xxx evaluateJavaScript 覆盖(15 项)** ← 本次重写
4. 弹层 settings.* polyfill(4 项)
5. WKUIDelegate alert/confirm(2 项)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
9f4ccb762e
commit
246f215d6f
@@ -766,6 +766,7 @@ 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 |
|
||||||
|
|
||||||
> **§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「典型案例」段。
|
||||||
|
|
||||||
|
|||||||
+43
-16
@@ -690,18 +690,31 @@ _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 注入(**已按原项目代码全面修订** — 见底部"修订记录")
|
### 4.2 JS 注入(**2026-06-22 重大路径调整:原生不写文件,只覆盖 `window.app_xxx` 全局变量**)
|
||||||
|
|
||||||
H5 业务通过 `<script src="app_*.js">` 同步引入 4 个预生成的 `.js` 文件,文件内全是 `var app_xxx=...` 全局变量声明。原 msext 写入位置:`NewRootVC.m:1204`(大厅 initJSdata)/ `gameController.m:2160`(子游戏 initJSdata)/ `gameController.m:1191` `NewRootVC.m:1706`(battery 更新)/ `gameController.m:1198` `NewRootVC.m:1714`(network 更新)/ `AppDelegate.m:259` `gameController.m:197`(gamesname 更新)。
|
H5 业务通过 `<script src="app_*.js">` 同步引入 4 个 `.js` 文件,文件内是 `var app_xxx = "<默认占位>"` 形式的全局变量声明。
|
||||||
|
|
||||||
**⚠️ 关键约束:文件名 ≠ 变量名**
|
**契约调整**(与原 msext 行为差异):
|
||||||
|
|
||||||
| 文件名(不带 get) | 文件内变量名(带 get) |
|
| 阶段 | msext 旧契约 | 新契约 |
|
||||||
|
|---|---|---|
|
||||||
|
| `app_*.js` 文件来源 | 原生 `NewRootVC.initJSdata` 用 `writeToFile:` 每次启动时写入沙盒 | **H5 团队随 `gamehall.zip` 一起打包**(带默认占位值),原生不写 |
|
||||||
|
| `app_xxx` 全局变量赋值 | H5 引入文件后由文件内的 `var` 声明赋值 | H5 引入文件后默认占位生效;原生用 `WKUserScript(.atDocumentEnd)` + `evaluateJavaScript` 覆盖 `window.app_xxx` 为实际值 |
|
||||||
|
| 路径变量 (`{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 历史)
|
||||||
|
|
||||||
|
| 文件名(不带 get) | 文件内变量名(带 get),原生需覆盖的 `window.xxx` |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `app_battery.js` | `var app_getbattery=...` |
|
| `app_battery.js` | `app_getbattery` |
|
||||||
| `app_network.js` | `var app_getnetwork=...` |
|
| `app_network.js` | `app_getnetwork` |
|
||||||
|
|
||||||
H5 端 `<script src="app_battery.js">` 引入后读到的是 `app_getbattery`(带 get 前缀),**不是** `app_battery`。任何拼写错误都会让 H5 业务读到 undefined。
|
H5 业务读 `app_getbattery`(带 get 前缀),**不是** `app_battery`。任何拼写错误都会让 H5 业务读到 H5 端的默认占位值(不是实际值)。
|
||||||
|
|
||||||
#### `app_data.js`(写到 `{gamedir}/{gamestart}/app_data.js`)
|
#### `app_data.js`(写到 `{gamedir}/{gamestart}/app_data.js`)
|
||||||
|
|
||||||
@@ -727,31 +740,42 @@ 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`(写到 `{gamedir}/{gamestart}/app_battery.js`)
|
#### `app_battery.js`(H5 zip 包内,含 `app_getbattery` 默认占位)
|
||||||
|
|
||||||
|
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 字面
|
||||||
```
|
```
|
||||||
|
|
||||||
更新时机:`viewWillAppear` + `UIDeviceBatteryLevelDidChangeNotification` + 前台切换通知。
|
原生覆盖时机(每次写一行 `window.app_getbattery = N;`):
|
||||||
|
- 首次 `webView didFinish` 后补一次(保证页面就绪即可读到当前电量)
|
||||||
|
- `UIDevice.batteryLevelDidChangeNotification` 触发
|
||||||
|
- `UIApplication.willEnterForegroundNotification`(前台切换补 retroact)
|
||||||
|
|
||||||
#### `app_network.js`(写到 `{gamedir}/{gamestart}/app_network.js`)
|
#### `app_network.js`(H5 zip 包内,含 `app_getnetwork` 默认占位)
|
||||||
|
|
||||||
|
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 蜂窝
|
||||||
```
|
```
|
||||||
|
|
||||||
更新时机:`viewWillAppear` + 网络变化(msext 用 AFNetworkReachability,新外壳用 NWPathMonitor)+ 前台切换通知。
|
原生覆盖时机:
|
||||||
|
- 首次 `webView didFinish` 后补一次
|
||||||
|
- `NWPathMonitor` 状态变化(msext 用 AFNetworkReachability,新外壳用 NWPathMonitor)
|
||||||
|
- 前台切换
|
||||||
|
|
||||||
#### `app_gamesname.js`(写到 `{gamedir}/{gamestart}/app_gamesname.js`)
|
#### `app_gamesname.js`(H5 zip 包内,含 `app_gamesname` 默认占位)
|
||||||
|
|
||||||
|
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 字面
|
||||||
```
|
```
|
||||||
|
|
||||||
更新时机:`AppDelegate.applicationDidFinishLaunching`(首次写入)+ 子游戏 `gameController.viewInit`(新装一个子游戏后追加;msext gameController.m:197 注释掉了实际写入逻辑,仅 AppDelegate 首次写入是活的)。
|
原生覆盖时机(合并进首次 documentEnd UserScript,即阶段 A):
|
||||||
|
- 首次启动覆盖
|
||||||
|
- 新装一个子游戏后下次进入大厅时覆盖(msext `gameController.m:197` 历史上注释掉了实际写入,仅 `AppDelegate` 首次有效;新外壳每次进入大厅都覆盖一次,更稳)
|
||||||
|
|
||||||
> H5 模板里 `index.html` 通过 `<script src="app_data.js">` 等同步引入这 4 个文件。新外壳必须保证这些文件**在 loadFileURL 之前**就写好,否则 H5 启动时变量未定义直接崩。
|
> ⚠️ **新外壳约定**:H5 团队保证 `gamehall.zip` 内 4 个 `app_*.js` 都存在,并提供合理默认占位(即使原生注入失败,H5 至少能读到默认值不崩)。原生不再保证"loadFileURL 之前文件已存在"——这是 H5 端打包责任。
|
||||||
|
|
||||||
#### 修订记录
|
#### 修订记录
|
||||||
|
|
||||||
@@ -760,6 +784,7 @@ 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 团队 / 项目方约定 |
|
||||||
|
|
||||||
### 4.3 WKUIDelegate(H5 alert / confirm)
|
### 4.3 WKUIDelegate(H5 alert / confirm)
|
||||||
|
|
||||||
@@ -979,10 +1004,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 个文件存在**:`{gamedir}/{gamestart}/` 下 `app_data.js` / `app_battery.js` / `app_network.js` / `app_gamesname.js` 全部生成、loadFileURL 前已落盘
|
- [ ] **`app_*.js` 4 个文件由 H5 zip 包内自带**:解压 `gamehall.zip` 后 `app_data.js` / `app_battery.js` / `app_network.js` / `app_gamesname.js` 全部存在(H5 团队打包责任,原生不写)
|
||||||
|
- [ ] **原生 `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 前缀)必须精确
|
||||||
- [ ] **大厅 vs 子游戏字面差异**:大厅 H5 内 `app_Launchtype === "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`
|
||||||
|
|
||||||
### B. 桥接
|
### B. 桥接
|
||||||
- [ ] H5 调 `accreditlogin` → 微信弹授权页 → 同意后 H5 收到 `sharelogin` 回调,7 字段完整
|
- [ ] H5 调 `accreditlogin` → 微信弹授权页 → 同意后 H5 收到 `sharelogin` 回调,7 字段完整
|
||||||
|
|||||||
@@ -1968,9 +1968,19 @@ for channel in xianliao 360 qq tencent; do
|
|||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.5 H5 `app_*.js` 预注入文件机制(契约 §4.2 + 原项目全面审查)
|
### 7.5 修改 H5 端 `app_*.js` 全局变量值(evaluateJavaScript 路径)
|
||||||
|
|
||||||
**用途**:H5 业务代码通过 `<script src="app_data.js">` / `<script src="app_battery.js">` / `<script src="app_network.js">` / `<script src="app_gamesname.js">` 等同步 `<script>` 引入预先写好的 4 个 `.js` 文件,文件内是若干 `var app_xxx = "..."` 全局变量声明。**这是 H5 在 iOS 9+ 路径下读渠道 / 设备 / 启动信息的唯一真实路径**(§3.4.1 旧桥同步 getter 已撤销)。
|
**用途**: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 在 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` 全局变量的值,**不再写文件**。理由:
|
||||||
|
>
|
||||||
|
> 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"
|
||||||
|
>
|
||||||
|
> 旧"写文件"骨架保留在 §7.5.3.legacy 作为对照参考,**不要实施**。
|
||||||
|
|
||||||
#### 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` 审查得出)
|
||||||
|
|
||||||
@@ -1992,25 +2002,175 @@ 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 注入时机与机制
|
||||||
|
|
||||||
```
|
**两阶段注入**:
|
||||||
{gamedir}/{gamestart}/app_data.js 一次性,loadFileURL 前(含 #1-#12 共 12 个 var)
|
|
||||||
{gamedir}/{gamestart}/app_gamesname.js 一次性 + 新装子游戏后增量重写(#13)
|
|
||||||
{gamedir}/{gamestart}/app_battery.js 每次 viewWillAppear / 前台 / batteryLevelDidChange(#14)
|
|
||||||
{gamedir}/{gamestart}/app_network.js 每次 viewWillAppear / NWPathMonitor 变化(#15)
|
|
||||||
```
|
|
||||||
|
|
||||||
**为什么是写文件而不是 `evaluateJavaScript`**:H5 业务页面用 `<script src="app_data.js">` **同步**引入,必须在 `loadFileURL` 之前文件已存在于磁盘;如果用 `evaluateJavaScript` 异步注入,会落在 `documentStart` 之前但 `<script src>` 已经发起请求,时序无保证。原 msext 30 个版本沿用"先写文件再 load"路径,不要照抄"WKUserScript 注入"。
|
| 阶段 | 时机 | 机制 | 覆盖的变量 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| **A. 首次覆盖** | `WKWebViewConfiguration` 装配阶段,`loadFileURL` 之前 | `WKUserScript(injectionTime: .atDocumentEnd, forMainFrameOnly: true)` | #1-#13 共 13 项(启动期已知的渠道 + 启动信息 + 已安装子游戏) |
|
||||||
|
| **B. 动态覆盖** | battery / network 变化时 / 进入 viewWillAppear | `webView.evaluateJavaScript("window.app_xxx = ...")` | #14 / #15 各自变化时单条 |
|
||||||
|
|
||||||
> 注:`WKUserScript(.atDocumentStart)` 也是可行的现代化方式(不依赖磁盘 IO),与"写文件"语义等价。两条路径选一即可。本 Design 推荐**写文件**路径,与 msext 一致,减少 H5 端任何假设差异;如 Phase 2 实施时发现写文件 IO 太慢(实测应该 < 5ms 不会成为瓶颈),再切 WKUserScript。
|
**为什么必须用 `.atDocumentEnd` 而不是 `.atDocumentStart`**:H5 的 `<script src="app_data.js">` 在 `documentStart` 之后被 fetch 执行,文件内的 `var app_xxx = "<default>"` 声明会**覆盖** documentStart 时我们注入的值。只有等 H5 自带的 `app_*.js` 都执行完(即 `documentEnd`)后再注入 `window.app_xxx = "实际值"` 才能稳定生效。
|
||||||
|
|
||||||
#### 7.5.3 Swift 骨架
|
> JS 内 `var x = ...` 在执行时只对当前作用域里**已经声明**的 `var` 做赋值;用 `window.x = ...` 直接修改全局对象的属性,与 `var` 声明语义等价但不会再被后续 `var` 重新声明 — 这是覆盖型注入的关键。
|
||||||
|
|
||||||
设计带可观察性:每次写文件都在 Debug 模式下把"写了哪个文件 + 内容预览 + 路径"打到控制台,便于联调时一眼对账 H5 端读到的值。Release 自动去 `.debug` 级别避免性能开销 + 防泄漏。
|
**为什么不直接让 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
|
```swift
|
||||||
// Source/WebView/AppDataWriter.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`(写文件路径,**不要实施**)
|
||||||
|
|
||||||
|
> 早期 §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。
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Source/WebView/AppDataWriter.swift —— 已撤销,请勿实施
|
||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -2158,15 +2318,17 @@ public struct AppDataWriter {
|
|||||||
private func runBootPipelineSteps() async throws {
|
private func runBootPipelineSteps() async throws {
|
||||||
// ... ensureReady / fetch / resolve / upgrade ...
|
// ... ensureReady / fetch / resolve / upgrade ...
|
||||||
|
|
||||||
// 6. loadFileURL 前写 4 个 app_*.js 文件(msext 沿用顺序)
|
let injector = AppDataInjector(
|
||||||
let writer = AppDataWriter(
|
|
||||||
bundleConfig: .shared,
|
bundleConfig: .shared,
|
||||||
resolvedVersion: resolved,
|
resolvedVersion: resolved,
|
||||||
containerRole: .lobby
|
containerRole: .lobby
|
||||||
)
|
)
|
||||||
try writer.writeInitial()
|
|
||||||
try writer.writeBattery(UIDevice.current.batteryLevel)
|
// 阶段 A:在 loadFileURL 前把首次覆盖 UserScript 加到 contentController。
|
||||||
try writer.writeNetwork(networkMonitor.currentCode)
|
// 脚本会在 H5 的 app_*.js var 声明执行完后跑(documentEnd),
|
||||||
|
// 覆盖 #1-#13 共 13 项启动期已知字段
|
||||||
|
let controller = bridgedWebView.webView.configuration.userContentController
|
||||||
|
controller.addUserScript(injector.makeInitialUserScript())
|
||||||
|
|
||||||
splash.update(text: "加载大厅...", progress: nil)
|
splash.update(text: "加载大厅...", progress: nil)
|
||||||
bridgedWebView.webView.loadFileURL(
|
bridgedWebView.webView.loadFileURL(
|
||||||
@@ -2174,16 +2336,35 @@ private func runBootPipelineSteps() async throws {
|
|||||||
allowingReadAccessTo: SandboxPaths.lobbyRoot
|
allowingReadAccessTo: SandboxPaths.lobbyRoot
|
||||||
)
|
)
|
||||||
|
|
||||||
// 持续监听:battery / network 变化时重写对应文件
|
// 阶段 B:H5 加载完后持续监听 battery / network 变化,
|
||||||
|
// 用 evaluateJavaScript 单条覆盖 window.app_getbattery / app_getnetwork
|
||||||
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
|
||||||
) { _ in
|
) { [weak self] _ in
|
||||||
try? writer.writeBattery(UIDevice.current.batteryLevel)
|
guard let self else { return }
|
||||||
|
Task { @MainActor in
|
||||||
|
await injector.injectBattery(UIDevice.current.batteryLevel,
|
||||||
|
into: self.bridgedWebView.webView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
networkMonitor.stateDidChange = { code in
|
networkMonitor.stateDidChange = { [weak self] code in
|
||||||
try? writer.writeNetwork(code)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -2196,89 +2377,97 @@ private func runBootPipelineSteps() async throws {
|
|||||||
|
|
||||||
#### 7.5.5 大厅 / 子游戏 / 弹层差异
|
#### 7.5.5 大厅 / 子游戏 / 弹层差异
|
||||||
|
|
||||||
| 容器 | app_data.js 写入 | app_gamesname.js 写入 | app_battery.js | app_network.js | 备注 |
|
| 容器 | 阶段 A UserScript 覆盖(13 项) | 阶段 B evaluateJavaScript 动态(2 项) | 备注 |
|
||||||
|------|---|---|---|---|---|
|
|------|---|---|---|
|
||||||
| 大厅(WebContainer.lobby) | `app_Launchtype="0"` + `gamedir = BundleConfig.gameDir` + `gamename = gameStart` | 写当前已安装子游戏列表 | ✓ | ✓ | `app_appversion` 用远端 ResolvedVersion |
|
| 大厅(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 |
|
| 子游戏(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 内 | 抽象为 `AppDataWriter` struct,三个容器复用同一份代码 |
|
| 注入主体 | `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` 全局变量 |
|
||||||
| 数据源 | `self.gameconfig` 等实例属性,跨多处赋值 | 统一从 `BundleConfig.shared` + `ResolvedVersion` 拉,单一真相 |
|
| 数据源 | `self.gameconfig` 等实例属性,跨多处赋值 | 统一从 `BundleConfig.shared` + `ResolvedVersion` 拉,单一真相 |
|
||||||
| 文件写入方式 | `[NSString writeToFile:atomically:YES encoding:UTF8 error:nil]` | `String.write(to:atomically:encoding:)`(等价) |
|
| battery / network 时机 | `viewWillAppear` + 前台通知双重保险 + 重写文件 | `addObserver` 注册一次 + `evaluateJavaScript` 单条覆盖 + 首次 didFinish 补一次 |
|
||||||
| battery / network 时机 | `viewWillAppear` + 前台通知双重保险 | 同一份机制 + `addObserver` 注册一次(避免双重写入) |
|
| 字符串包裹符 | `var x='%@'` 单引号,沿用 msext 字面 | `window.x='...'` 单引号一致 |
|
||||||
| 转义处理 | 直接 `stringWithFormat:@"var x=\"%@\""` 不转义 | 完整转义 4 种特殊字符(避免渠道字段含引号导致 H5 JS 解析错) |
|
| 转义处理 | 直接 `stringWithFormat:@"var x='%@'"` 不转义 | 完整转义 4 种特殊字符(避免渠道字段含单引号导致 H5 JS 解析错) |
|
||||||
| 大小写 | `app_Launchtype` / `app_getwifisignalLevel` 严格保持 | 同款,注释提示 |
|
| 大小写 | `app_Launchtype` / `app_getwifisignalLevel` 严格保持 | 同款,注释提示 |
|
||||||
|
|
||||||
#### 7.5.7 与 §3.4.1 撤销 polyfill 的关系
|
#### 7.5.7 与 §3.4.1 撤销 polyfill 的关系
|
||||||
|
|
||||||
| 路径 | 状态 | 用途 |
|
| 路径 | 状态 | 用途 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| §7.5 `app_*.js` 文件预写 | ✓ **主路径** | iOS 9+ 全部 H5 业务读 `app_xxx` 全局变量 |
|
| §7.5 `app_*.js` evaluateJavaScript 修改全局变量 | ✓ **主路径** | iOS 9+ 全部 H5 业务读 `app_xxx` 全局变量;H5 自带 `app_*.js` 默认占位,原生覆盖为实际值 |
|
||||||
| §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 文件重写互为补充 |
|
| §3.2 事件 callback `getBattery` / `getnetwork` / `appservice` | ✓ **保留** | 业务期 H5 主动收到的变化推送,与 §7.5 `evaluateJavaScript` 覆盖互为补充(推送 + 状态双轨) |
|
||||||
|
| msext 原写文件路径 | ✗ **不实施** | §7.5.3.legacy 保留作对照参考 |
|
||||||
|
|
||||||
#### 7.5.8 联调时的可观察性(Phase 1.17 调试手段)
|
#### 7.5.8 联调时的可观察性(Phase 1.17 调试手段)
|
||||||
|
|
||||||
H5 端读到 undefined / 值不对时,第一步是确认"原生写了什么、H5 读到什么"。三条对账路径:
|
H5 端读到错误值时,第一步是确认"原生注入了什么、H5 端最终读到什么"。**核心对账**:原生注入的 JS 字符串与 H5 端 `window.app_xxx` 的实时值。
|
||||||
|
|
||||||
**1. Xcode 控制台 — 原生写入 log(§7.5.3 已埋点)**
|
**1. Xcode 控制台 — 原生注入 log(§7.5.3 已埋点)**
|
||||||
|
|
||||||
启动后 Xcode console 应出现:
|
启动后 Xcode console 应出现:
|
||||||
|
|
||||||
```
|
```
|
||||||
[lobby] write app_data.js → /Users/.../Caches/<gamedir>/<gamestart>/app_data.js
|
[lobby] makeInitialUserScript (0 games installed):
|
||||||
app_version = 1
|
window.app_version = 1
|
||||||
app_gameconfig = 'tsgames.daoqi88.cn-config_test-update_jsonv2_test'
|
window.app_gameconfig = 'tsgames.daoqi88.cn-config_test-update_jsonv2_test'
|
||||||
app_gamedir = 'FtJf07...'
|
window.app_gamedir = 'FtJf07...'
|
||||||
app_gamestart = 'gamehall'
|
window.app_gamestart = 'gamehall'
|
||||||
...
|
window.app_agent = 'veRa0qrBf0df...'
|
||||||
app_Launchtype = 0
|
window.app_appversion = '43'
|
||||||
app_getwifisignalLevel = 1
|
window.app_market = '2'
|
||||||
app_gamename = 'gamehall'
|
window.app_channel = 'FtJf07...'
|
||||||
app_invitationcode = ''
|
window.app_Launchtype = 0
|
||||||
[lobby] write app_gamesname.js: 0 games → /Users/.../Caches/<gamedir>/<gamestart>/app_gamesname.js
|
window.app_getwifisignalLevel = 1
|
||||||
games = []
|
window.app_gamename = 'gamehall'
|
||||||
[lobby] write app_battery.js: app_getbattery=0.85 → ...
|
window.app_invitationcode = ''
|
||||||
[lobby] write app_network.js: app_getnetwork=2 → ...
|
window.app_gamesname = []
|
||||||
|
[lobby] inject battery: app_getbattery=0.85
|
||||||
|
[lobby] inject network: app_getnetwork=2
|
||||||
```
|
```
|
||||||
|
|
||||||
**2. 沙盒文件直接读 — 等价 `cat`**
|
若没出现 → AppDataInjector 没接入 / log subsystem 过滤掉了,先按 §7.5.4 接入点检查。
|
||||||
|
|
||||||
如果控制台 log 没问题但 H5 端仍读 undefined,去沙盒拿实际文件验证写入和"H5 看到的"是否一致:
|
**2. H5 console 对账 — Safari Web Inspector(核心手段)**
|
||||||
|
|
||||||
```bash
|
模拟器跑时 macOS Safari → 开发菜单 → Simulator → 当前 H5 页面 → Web Inspector → Console,逐项 console.log 读 `window.app_xxx`:
|
||||||
# 模拟器路径
|
|
||||||
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,逐项 console.log:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
console.log("app_version =", app_version); // 期望 1
|
// 阶段 A:13 项启动期覆盖
|
||||||
console.log("app_channel =", app_channel); // 期望 ChannelConfig.plist channel 值
|
console.log("app_version =", window.app_version); // 期望 1
|
||||||
console.log("app_Launchtype =", app_Launchtype); // 大厅期望 0,⚠️ L 大写
|
console.log("app_channel =", window.app_channel); // 期望 ChannelConfig.plist channel 值
|
||||||
console.log("app_getbattery =", app_getbattery); // ⚠️ 带 get 前缀
|
console.log("app_Launchtype =", window.app_Launchtype); // 大厅期望 0,⚠️ L 大写
|
||||||
console.log("app_getnetwork =", app_getnetwork);
|
console.log("app_getwifisignalLevel =", window.app_getwifisignalLevel); // ⚠️ wifi 小写
|
||||||
console.log("app_gamesname =", app_gamesname); // 期望 Array
|
console.log("app_invitationcode =", window.app_invitationcode);
|
||||||
|
console.log("app_gamesname =", window.app_gamesname); // 期望 Array
|
||||||
|
|
||||||
|
// 阶段 B:2 项动态覆盖
|
||||||
|
console.log("app_getbattery =", window.app_getbattery); // ⚠️ 带 get 前缀
|
||||||
|
console.log("app_getnetwork =", window.app_getnetwork);
|
||||||
```
|
```
|
||||||
|
|
||||||
任一项为 `undefined` → 拼写错误(大小写 / 前缀)或文件没加载到。常见错误:写成 `app_launchtype`(小写 l)/ `app_battery`(少 get)/ `app_getNetwork`(驼峰错)。
|
| 现象 | 可能原因 |
|
||||||
|
|---|---|
|
||||||
|
| 全部为 H5 端默认占位(不是实际渠道值)| AppDataInjector.makeInitialUserScript 没 addUserScript / injectionTime 错(不是 .atDocumentEnd)/ H5 自带 `app_*.js` 的 var 声明在我们之后又跑了一次 |
|
||||||
|
| 某项为 `undefined` | 拼写错误(`app_launchtype` 小写 l / `app_battery` 少 get / `app_getNetwork` 驼峰错) |
|
||||||
|
| `app_getbattery` / `app_getnetwork` 是默认值不变 | 阶段 B 钩子没接入 / 模拟器 batteryLevel = -1(模拟器无电池硬件)/ NWPathMonitor 未启动 |
|
||||||
|
| `app_gamesname` 是空数组但实际已装子游戏 | `SandboxPaths.installedSubGameNames()` 实现错 / 子游戏目录路径不对 |
|
||||||
|
|
||||||
**4. 真机调试**:iPhone 上 Safari → 设置 → 高级 → 网页检查器开启,然后 macOS Safari 同样路径。
|
**3. 真机调试**: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 再读应反映新值。
|
||||||
|
|
||||||
|
> ⚠️ 上面 H5 控制台和原生 log 调试**仅在 Debug 配置生效**。Release 版本 os.log `.debug` 级别会被系统过滤掉,避免性能开销和敏感字段(agent / channel)外泄。Phase 9 Release 监控走 Sentry breadcrumb(§11.3),不依赖 console。
|
||||||
|
|
||||||
> ⚠️ 上面 H5 控制台和文件路径调试**仅在 Debug 配置生效**。Release 版本 os.log `.debug` 级别会被系统过滤掉,避免性能开销和敏感字段(agent / channel)外泄。Phase 9 Release 监控走 Sentry breadcrumb(§11.3),不依赖 console。
|
> ⚠️ 上面 H5 控制台和文件路径调试**仅在 Debug 配置生效**。Release 版本 os.log `.debug` 级别会被系统过滤掉,避免性能开销和敏感字段(agent / channel)外泄。Phase 9 Release 监控走 Sentry breadcrumb(§11.3),不依赖 console。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user