Compare commits
3
Commits
1203957924
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
707abcb9d3 | ||
|
|
7fe089577b | ||
|
|
f4acccdd6c |
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: daoqi-lessons
|
||||
description: 从原项目 daoqi/msext 排查出来的典型案例存档(LaunchScreen 启动图方向错误、H5 与原生通讯接口的真实路径)。在处理启动图 / LaunchScreen / 启动方向问题,或处理 H5 读取渠道值、app_*.js 变量、window.settings polyfill 相关问题时阅读。
|
||||
---
|
||||
|
||||
# 原项目 daoqi 排查案例存档
|
||||
|
||||
> 这些是从 `../daoqi/` 实际排查出来的完整案例,佐证 CLAUDE.md「原项目 daoqi:遇到问题时的参考来源」一节的方法论。
|
||||
> 注意:`典型案例:app_* 注入时序问题(原则 A 不可妥协)` 仍保留在 CLAUDE.md 正文中,因为它是原则 A 的规范性说明,必须常驻。
|
||||
|
||||
## 典型案例:LaunchScreen orientation(启动图方向错误)
|
||||
|
||||
- **症状**:新项目启动图渲染成竖向矩形再被整体旋转 90°,画面横躺在中间
|
||||
- **错误做法**:凭 Xcode 26 文档推测、纯调整 storyboard / contentMode / 反复换素材
|
||||
- **去原项目找答案**:`daoqi/msext/Info.plist` + `msext.xcodeproj/project.pbxproj`
|
||||
- 发现 1:Info.plist 用的是**顶层无后缀的 `UISupportedInterfaceOrientations`**(值 = landscape),**没有** `~iphone` / `~ipad` 后缀变体
|
||||
- 发现 2:根本**没有** `UILaunchStoryboardName`,用的是 `ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "LaunchImage-1"`(iOS 8 时代的 LaunchImage Asset Catalog 机制)
|
||||
- **原项目为什么稳**:
|
||||
- 它的所有 LaunchImage PNG(`Default-568h@2x` / `LaunchImage-1-800-667h@2x` 等)**物理像素都是竖向、画面内容横躺**
|
||||
- iOS 8 LaunchImage Asset Catalog 内置"按 device idiom + orientation 自动选图 + 系统级 90° 旋转"魔法,会读 Info.plist 顶层 `UISupportedInterfaceOrientations` 决定是否旋转 portrait 资源
|
||||
- 因此一张物理 portrait 的 PNG 在横屏设备上能被正确旋转后铺满
|
||||
- **不能照搬到新项目**:
|
||||
- LaunchImage Asset Catalog 在 iOS 14+ 已 deprecated,新项目按苹果推荐用 `LaunchScreen.storyboard`
|
||||
- LaunchScreen.storyboard **没有自动旋转 portrait 资源**的魔法,UIImageView 直接渲染 PNG 物理像素
|
||||
- 把 `docs/res/Res/Default-568h@2x` 这类**物理 portrait + 画面横躺**的 msext 残留素材直接塞进 LaunchScreen.storyboard 就会看到躺倒画面
|
||||
- **新项目(LaunchScreen.storyboard)的正确组合**:
|
||||
1. **素材必须物理像素就是横屏的**(必要时用 `sips -r -90 -s format png` 一次性把 msext 残留 portrait 素材逆时针 90° 输出为标准 PNG,物理像素正确)
|
||||
2. **Info.plist 显式补一份无后缀 `UISupportedInterfaceOrientations = landscape`**(Xcode 26 General → Deployment Info UI 只写带后缀的 `~iphone` / `~ipad`,少了无后缀 key 会让 LaunchScreen 阶段——device idiom 尚未识别的早期窗口——fallback 到 portrait 渲染再被旋转)
|
||||
3. UIImageView `contentMode = scaleAspectFit` + 黑底(比 16:9 更宽的现代横屏设备左右补黑边,保画面完整不裁切 logo)
|
||||
- **iOS launch snapshot 缓存粘滞(开发期 iteration 坑)**:
|
||||
- iOS 为加速启动,会把首次渲染的 LaunchScreen 缓存为 PNG snapshot 存到 app sandbox(`Library/Caches/Snapshots/<bundleID>/com.apple.UIKit.SplashBoard/`),后续启动不重渲染
|
||||
- **Xcode 覆盖安装 .app 不会清 sandbox 缓存**,所以改了 Info.plist / LaunchScreen.storyboard / 启动图素材后,Run 出来仍可能是旧 snapshot
|
||||
- 改完启动相关任意一项**必须做**:长按 app → 删除 → Xcode 重新 Run;或模拟器 Erase All Content and Settings;或 `xcrun simctl uninstall booted <bundleID>`
|
||||
- **终端用户不受影响**:从 IPA 首次安装、或升级新 IPA 第一次启动后即被 iOS 自动刷新;只是开发期反复迭代同一台 device 时会被迷惑
|
||||
|
||||
## 典型案例:H5 与原生通讯接口的真实路径
|
||||
|
||||
- **症状**:Design / Contract 文档描述 H5 用 `window.settings.getXxx()` 同步函数读渠道值(如 `getothername` / `getchannelName`);按文档实现完 polyfill 后 H5 业务行为不对,user 反馈"H5 读不到数据"
|
||||
- **错误做法**:基于 Contract §附录 A 列出的旧桥 JSExport 28 方法实现 polyfill,把 9 项 getter 注入到 `window.settings`,认为这是 H5 端的主路径
|
||||
- **去原项目找答案**:grep `var app_` 在 `daoqi/msext/Class/RootVC/NewRootVC.m:1204` / `gameController.m:2160` / `AppDelegate.m:259`
|
||||
- 发现 1:原项目 `initJSdata()` 是 `[NSString stringWithFormat:@"var app_xxx=...;"]` + `writeToFile:` **写 .js 文件到沙盒**,H5 业务用 `<script src="app_data.js">` 同步引入预生成的 12 个全局变量直接读
|
||||
- 发现 2:Contract §附录 A 自身标注「iOS<9 路径使用的旧协议,新外壳如最低系统 ≥ iOS 14 可不实现」,新项目最低 iOS 15.6 → 旧桥 polyfill **完全不必要**
|
||||
- 发现 3:Contract §4.2 之前列的 11 项变量与原项目代码不一致 — 漏 6 项(version / Launchtype / getwifisignalLevel / gamename / invitationcode / gamesname)、多 2 项(gameid / compareCode 原项目根本不写)、文件名与变量名混淆(文件 `app_battery.js` 内的变量名是带 get 的 `app_getbattery`,文件名不带 get)
|
||||
- **修复**:
|
||||
1. 撤销已实现的 `window.settings.getXxx()` polyfill 章节(Design §3.4.1)
|
||||
2. 新增 Design §7.5「H5 `app_*.js` 预注入文件机制」覆盖 15 个变量的写入逻辑
|
||||
3. 按原项目代码全面修订 Contract §4.2(删 gameid/compareCode、补 6 项、修正 battery/network 变量名带 get、注明大厅/子游戏字面差异、加修订记录小节)
|
||||
- **教训**:
|
||||
- **文档与代码冲突时,原项目代码是真理**。Contract 也可能写错(一份 10 年前写就的文档遗漏 / 误传是常态),必须用 `grep` 验证字面字符串
|
||||
- **优先验证字面字符串、不要依赖语义猜测**:`app_getbattery` 与 `app_battery` 一字之差,AI 助手凭语义"应该就是 app_battery 吧" 100% 会犯错
|
||||
- **iOS 最低系统版本是契约边界的天然过滤器**:契约里 iOS<9 路径明示可不实现的接口(如 §附录 A 旧桥),新项目最低 iOS 15.6 应当主动剔除,不要"出于完备性"反而实现一份用不到的 polyfill
|
||||
@@ -51,47 +51,14 @@ daoqi 仓库当前并存两条工作线:
|
||||
- 参考原项目 = 把它当作 **"哪些事必须做"的清单** 和 **"哪些坑已经踩过"的备忘**
|
||||
- 新项目的现代化重构(Swift 6 / SwiftUI / actor 隔离 / SPM 依赖等)依然按原则 B 自由设计;只是设计前先确认"我没有漏掉原项目实际需要解决的事"
|
||||
|
||||
### 典型案例:LaunchScreen orientation(启动图方向错误)
|
||||
### 案例存档(按需读取)
|
||||
|
||||
- **症状**:新项目启动图渲染成竖向矩形再被整体旋转 90°,画面横躺在中间
|
||||
- **错误做法**:凭 Xcode 26 文档推测、纯调整 storyboard / contentMode / 反复换素材
|
||||
- **去原项目找答案**:`daoqi/msext/Info.plist` + `msext.xcodeproj/project.pbxproj`
|
||||
- 发现 1:Info.plist 用的是**顶层无后缀的 `UISupportedInterfaceOrientations`**(值 = landscape),**没有** `~iphone` / `~ipad` 后缀变体
|
||||
- 发现 2:根本**没有** `UILaunchStoryboardName`,用的是 `ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "LaunchImage-1"`(iOS 8 时代的 LaunchImage Asset Catalog 机制)
|
||||
- **原项目为什么稳**:
|
||||
- 它的所有 LaunchImage PNG(`Default-568h@2x` / `LaunchImage-1-800-667h@2x` 等)**物理像素都是竖向、画面内容横躺**
|
||||
- iOS 8 LaunchImage Asset Catalog 内置"按 device idiom + orientation 自动选图 + 系统级 90° 旋转"魔法,会读 Info.plist 顶层 `UISupportedInterfaceOrientations` 决定是否旋转 portrait 资源
|
||||
- 因此一张物理 portrait 的 PNG 在横屏设备上能被正确旋转后铺满
|
||||
- **不能照搬到新项目**:
|
||||
- LaunchImage Asset Catalog 在 iOS 14+ 已 deprecated,新项目按苹果推荐用 `LaunchScreen.storyboard`
|
||||
- LaunchScreen.storyboard **没有自动旋转 portrait 资源**的魔法,UIImageView 直接渲染 PNG 物理像素
|
||||
- 把 `docs/res/Res/Default-568h@2x` 这类**物理 portrait + 画面横躺**的 msext 残留素材直接塞进 LaunchScreen.storyboard 就会看到躺倒画面
|
||||
- **新项目(LaunchScreen.storyboard)的正确组合**:
|
||||
1. **素材必须物理像素就是横屏的**(必要时用 `sips -r -90 -s format png` 一次性把 msext 残留 portrait 素材逆时针 90° 输出为标准 PNG,物理像素正确)
|
||||
2. **Info.plist 显式补一份无后缀 `UISupportedInterfaceOrientations = landscape`**(Xcode 26 General → Deployment Info UI 只写带后缀的 `~iphone` / `~ipad`,少了无后缀 key 会让 LaunchScreen 阶段——device idiom 尚未识别的早期窗口——fallback 到 portrait 渲染再被旋转)
|
||||
3. UIImageView `contentMode = scaleAspectFit` + 黑底(比 16:9 更宽的现代横屏设备左右补黑边,保画面完整不裁切 logo)
|
||||
- **iOS launch snapshot 缓存粘滞(开发期 iteration 坑)**:
|
||||
- iOS 为加速启动,会把首次渲染的 LaunchScreen 缓存为 PNG snapshot 存到 app sandbox(`Library/Caches/Snapshots/<bundleID>/com.apple.UIKit.SplashBoard/`),后续启动不重渲染
|
||||
- **Xcode 覆盖安装 .app 不会清 sandbox 缓存**,所以改了 Info.plist / LaunchScreen.storyboard / 启动图素材后,Run 出来仍可能是旧 snapshot
|
||||
- 改完启动相关任意一项**必须做**:长按 app → 删除 → Xcode 重新 Run;或模拟器 Erase All Content and Settings;或 `xcrun simctl uninstall booted <bundleID>`
|
||||
- **终端用户不受影响**:从 IPA 首次安装、或升级新 IPA 第一次启动后即被 iOS 自动刷新;只是开发期反复迭代同一台 device 时会被迷惑
|
||||
以下两个完整案例已移到 `.claude/skills/daoqi-lessons/SKILL.md`(用到时再加载,不常驻上下文):
|
||||
|
||||
### 典型案例:H5 与原生通讯接口的真实路径
|
||||
- **典型案例:LaunchScreen orientation(启动图方向错误)** — 处理启动图 / 启动方向问题时读
|
||||
- **典型案例:H5 与原生通讯接口的真实路径** — 处理 H5 读渠道值 / `app_*.js` 变量 / `window.settings` polyfill 时读
|
||||
|
||||
- **症状**:Design / Contract 文档描述 H5 用 `window.settings.getXxx()` 同步函数读渠道值(如 `getothername` / `getchannelName`);按文档实现完 polyfill 后 H5 业务行为不对,user 反馈"H5 读不到数据"
|
||||
- **错误做法**:基于 Contract §附录 A 列出的旧桥 JSExport 28 方法实现 polyfill,把 9 项 getter 注入到 `window.settings`,认为这是 H5 端的主路径
|
||||
- **去原项目找答案**:grep `var app_` 在 `daoqi/msext/Class/RootVC/NewRootVC.m:1204` / `gameController.m:2160` / `AppDelegate.m:259`
|
||||
- 发现 1:原项目 `initJSdata()` 是 `[NSString stringWithFormat:@"var app_xxx=...;"]` + `writeToFile:` **写 .js 文件到沙盒**,H5 业务用 `<script src="app_data.js">` 同步引入预生成的 12 个全局变量直接读
|
||||
- 发现 2:Contract §附录 A 自身标注「iOS<9 路径使用的旧协议,新外壳如最低系统 ≥ iOS 14 可不实现」,新项目最低 iOS 15.6 → 旧桥 polyfill **完全不必要**
|
||||
- 发现 3:Contract §4.2 之前列的 11 项变量与原项目代码不一致 — 漏 6 项(version / Launchtype / getwifisignalLevel / gamename / invitationcode / gamesname)、多 2 项(gameid / compareCode 原项目根本不写)、文件名与变量名混淆(文件 `app_battery.js` 内的变量名是带 get 的 `app_getbattery`,文件名不带 get)
|
||||
- **修复**:
|
||||
1. 撤销已实现的 `window.settings.getXxx()` polyfill 章节(Design §3.4.1)
|
||||
2. 新增 Design §7.5「H5 `app_*.js` 预注入文件机制」覆盖 15 个变量的写入逻辑
|
||||
3. 按原项目代码全面修订 Contract §4.2(删 gameid/compareCode、补 6 项、修正 battery/network 变量名带 get、注明大厅/子游戏字面差异、加修订记录小节)
|
||||
- **教训**:
|
||||
- **文档与代码冲突时,原项目代码是真理**。Contract 也可能写错(一份 10 年前写就的文档遗漏 / 误传是常态),必须用 `grep` 验证字面字符串
|
||||
- **优先验证字面字符串、不要依赖语义猜测**:`app_getbattery` 与 `app_battery` 一字之差,AI 助手凭语义"应该就是 app_battery 吧" 100% 会犯错
|
||||
- **iOS 最低系统版本是契约边界的天然过滤器**:契约里 iOS<9 路径明示可不实现的接口(如 §附录 A 旧桥),新项目最低 iOS 15.6 应当主动剔除,不要"出于完备性"反而实现一份用不到的 polyfill
|
||||
下面保留的 `app_*` 注入时序案例是原则 A 的规范性说明,常驻不移出。
|
||||
|
||||
### 典型案例:`app_*` 注入时序问题(原则 A 不可妥协)
|
||||
|
||||
|
||||
+76
-3
@@ -172,11 +172,42 @@ Safari "开发" 菜单会把每个**已加载页面**的 `WKWebView` 列为**独
|
||||
|
||||
### Q3:能看到条目但 Console 是空的
|
||||
|
||||
可能原因:
|
||||
**首要原因:H5 自带的调试总开关出厂是关的** ⭐
|
||||
|
||||
`gamehall.zip` → `js/01_SubGame/00_SubGame_Config.js:1-12`:
|
||||
|
||||
```js
|
||||
Game_Config.Debugger={
|
||||
isDebugger : false, // debugger模式下会将所有收发的包输出到控制台(正式发布改为false)
|
||||
...
|
||||
```
|
||||
|
||||
整包 220 处 `console.log`,82 处已被 `//` 注释掉,剩下的**高频日志全部挡在这个开关后面**:
|
||||
|
||||
| 位置 | 内容 |
|
||||
|------|------|
|
||||
| `js/00_Surface/09_Net.js:47` | `发送数据:...` |
|
||||
| `js/00_Surface/12_Logic.js:210` | `接收数据:...` |
|
||||
| `js/00_Surface/12_Logic.js:463 / 1317` | 包体输出 |
|
||||
|
||||
没被挡的只剩 `12_Logic.js:784/829/870/921` 的 `Connect:...`,那几行在大厅启动建 websocket 时就打完了,等你挂上 Inspector 早已过去。
|
||||
|
||||
**打开方式(不改 H5 一个字节,符合原则 A)** —— 在 Web Inspector 控制台运行:
|
||||
|
||||
```js
|
||||
Game_Config.Debugger.isDebugger = true
|
||||
```
|
||||
|
||||
这是运行时改内存里的对象属性,不碰 `gamehall.zip`、不碰任何文件;杀进程重启即恢复 `false`。
|
||||
|
||||
> ⚠️ **不要去改 `Resources/gamehall.zip` 里的这个 `false`** —— 那是 H5 修改,违反原则 A,且会跟着渠道包发出去。
|
||||
|
||||
其它可能原因:
|
||||
|
||||
- H5 端确实没有 `console.*` 调用(业务在沉默运行,没问题就不输出)
|
||||
- Web Inspector 打开前的旧日志没被记录 → 在 Web Inspector 已经打开的状态下**触发一次** H5 操作(点按钮、切 tab)再看
|
||||
- Console 顶部过滤器把日志级别过滤掉了 → 确认 All Levels 都选中
|
||||
- 连错 WebView(3 个实例 title 都是 `gameabc`)→ 敲 `typeof Game_Config`,返回 `"undefined"` 说明不是大厅那个,按 §5.2 用 hover 高亮法重选
|
||||
- H5 端确实没有 `console.*` 调用(业务在沉默运行,没问题就不输出)
|
||||
|
||||
### Q4:Sources 里看不到 .js 源码
|
||||
|
||||
@@ -200,9 +231,49 @@ Safari Web Inspector 是开发期主力,但有它够不到的场景:
|
||||
|
||||
- **真机 + 用户已在生产环境**:Release 包没有 inspectable;需要的是事后日志,参考原生侧的崩溃 / 错误上报(项目目前无 crash 监控,依赖用户反馈)
|
||||
- **断网 / 弱网下的 H5 表现**:用 Mac 上的 Network Link Conditioner,或模拟器 Features → Network Link Conditioner
|
||||
- **生产环境的 H5 console 输出**:原生侧已经装了 `H5ErrorRelay`(`Source/Bridge/H5ErrorRelay.swift`),会把 `console.error / window.onerror / unhandledrejection` 转发到原生 `print`,Debug build 可在 Xcode 控制台看到。要扩展到 `console.log` 全量转发,可参考其实现照葫芦画瓢
|
||||
- **不想开 Safari、只在 Xcode 控制台看 H5 输出**:原生侧的 `H5ErrorRelay`(`Source/Bridge/H5ErrorRelay.swift`)已覆盖,见下面 §8.1
|
||||
- **断点调试原生 Swift 与 H5 同时跑**:Xcode + Safari Web Inspector 同时打开,两边各自下断点互不影响
|
||||
|
||||
### 8.1 H5 console → Xcode 控制台(`H5ErrorRelay`)
|
||||
|
||||
`Source/Bridge/H5ErrorRelay.swift` 用 `WKUserScript(.atDocumentStart)` hook 住 console 与全局异常,通过 `webkit.messageHandlers.h5error` 转发到原生。**不改 H5 一行**(原则 A)。
|
||||
|
||||
前缀里的 `lobby` / `subGame` 是 `BridgedWebView(label:)`,用来区分同时存在的多个 WebView——也用来反推该在 Safari「开发」菜单里选哪个条目。
|
||||
|
||||
| 来源 | Xcode 输出前缀 | 落 `Documents/diag.log` | 构建配置 |
|
||||
|-------|---------------|------------------------|---------|
|
||||
| `console.log` | `[H5:lobby console.log]` | ❌ | **仅 Debug** |
|
||||
| `console.info` | `[H5:lobby console.info]` | ❌ | **仅 Debug** |
|
||||
| `console.warn` | `[H5:lobby console.warn]` | ✅ | Debug + Release |
|
||||
| `console.error` | `[H5:lobby console.error]` | ✅ | Debug + Release |
|
||||
| 未捕获 JS 异常 | `[H5:lobby onerror]` | ✅ | Debug + Release |
|
||||
| 未处理的 Promise rejection | `[H5:lobby unhandledrejection]` | ✅ | Debug + Release |
|
||||
| 资源 404(`<script>`/`<img>`/`<audio>`) | `[H5:lobby resourceerror]` | ✅ | Debug + Release |
|
||||
| WebView 首次/提交后加载失败 | `[WebView:lobby] …加载失败` | ✅ | Debug + Release |
|
||||
| WebContent 进程终止(白屏) | `[WebView:lobby] ⚠️ WebContent 进程终止` | ✅ | Debug + Release |
|
||||
|
||||
### 8.2 仍然抓不到的(已知盲区)
|
||||
|
||||
| 场景 | 为什么抓不到 |
|
||||
|------|------------|
|
||||
| 被 H5 自己 `try/catch` 吞掉的异常 | 根本不冒到 window。本包里 `12_Logic.js:254` / `gameabc.min.js:2175` 就是 catch 后用 `console.log(e.stack)` 打印 → 只在 Debug 的 `[H5:… console.log]` 里能看到 |
|
||||
| iframe 内的异常 | `WKUserScript(forMainFrameOnly: true)`,只 hook 主框架 |
|
||||
| 弹层(`OverlayViewController`)的 JS | 第三方外链,按设计不挂 relay |
|
||||
| 跨域脚本的异常细节 | 浏览器安全策略统一报 `Script error.`,无行号无堆栈 |
|
||||
| 原生 handler 内部抛错 | 属原生侧,走 `BridgeBus` 自己的 `diagLog` |
|
||||
|
||||
### 8.3 设计要点
|
||||
|
||||
- **资源 404 必须开捕获阶段**:`<script>`/`<img>`/`<audio>` 的 error 事件只在元素自身触发且**不冒泡**,`addEventListener('error', fn)` 收不到,必须 `addEventListener('error', fn, true)`。JS 异常的 `ev.target` 是 `window`,据此在同一 listener 内分流两类
|
||||
- **导航层失败只 log 不自动 reload**:自动恢复是行为变化,msext 没有,不引入
|
||||
- **`log` / `info` 只在 Debug 注入**:Release 包不该为每条业务日志付一次 JS→Native IPC,也不该把 H5 内部输出暴露给外部审阅。`#if DEBUG` 在编译期就把这段 JS 从 `javaScriptSource` 里摘掉
|
||||
- **`log` / `info` 走裸 `print` 而非 `diagLog`**:`diagLog` 会同步落盘到 `Documents/diag.log`,而该 sink 无轮转、无大小上限(`BridgeBus.swift:47`)。一旦 §7 Q3 里的 `isDebugger` 被打开,收发包 firehose 灌进去会把设备磁盘吃光
|
||||
- **对象参数走 `JSON.stringify`**:大厅业务大量使用 `console.log(msg)` / `console.log(res)` 打整包,直接 `String(obj)` 只会得到无用的 `[object Object]`。循环引用时 stringify 抛错 → 回退 `String()` → 再抛则输出 `[unstringifiable]`
|
||||
- **原始 console 调用被透传**(`origLog.apply(console, arguments)`),所以转发不影响 Safari Web Inspector 里照常看到日志,两条路可以同时用
|
||||
- **仅 `BridgedWebView`(大厅 + 子游戏)注入**,`OverlayViewController` 是第三方外链,不挂
|
||||
|
||||
⚠️ 依然受 §7 Q3 的 `Game_Config.Debugger.isDebugger` 制约:收发包日志被 H5 自己的开关挡着,Xcode 控制台同样看不到。需要时在 Safari 控制台运行 `Game_Config.Debugger.isDebugger = true`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 修订记录
|
||||
@@ -210,3 +281,5 @@ Safari Web Inspector 是开发期主力,但有它够不到的场景:
|
||||
| 日期 | 内容 |
|
||||
|------|------|
|
||||
| 2026-06-24 | 文档建立。落地 `BridgedWebView` / `OverlayViewController` 两处 `isInspectable` 补丁。 |
|
||||
| 2026-08-08 | 查明「Console 空」的首要原因是 H5 自带 `Game_Config.Debugger.isDebugger = false`,补进 §7 Q3。`H5ErrorRelay` 扩展 `console.log` / `console.info` 转发(Debug only)+ 对象参数 JSON 序列化,新增 §8.1。 |
|
||||
| 2026-08-08 | 补齐「非 H5 主动 console.error」的报错通路:资源 404(error 事件捕获阶段)、WebView 加载失败 / WebContent 进程终止(两个 VC 的 `WKNavigationDelegate`,此前完全没实现)。输出加 `lobby` / `subGame` 前缀。新增 §8.2 已知盲区、§8.3 设计要点。 |
|
||||
|
||||
@@ -208,9 +208,11 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = NX5W3B4QP3;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NX5W3B4QP3;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Vendor/AMap",
|
||||
@@ -242,6 +244,8 @@
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.skyapp.ylgamehall;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "iOS dev";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
@@ -258,9 +262,12 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = NX5W3B4QP3;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NX5W3B4QP3;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Vendor/AMap",
|
||||
@@ -292,6 +299,8 @@
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.skyapp.ylgamehall;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ios_ad_hoc;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<key>gameid</key>
|
||||
<string>G2hw0ubng0zcoI0r4mx3H2yr4GejidwO</string>
|
||||
<key>channel</key>
|
||||
<string>frdt0C1GG0t91P0McFo0rbA1he5yurbS</string>
|
||||
<string>aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG</string>
|
||||
<key>gamedir</key>
|
||||
<string>frdt0C1GG0t91P0McFo0rbA1he5yurbS</string>
|
||||
<string>aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG</string>
|
||||
<key>gamestart</key>
|
||||
<string>gamehall</string>
|
||||
<key>gameconfig</key>
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>market</key>
|
||||
<string>2</string>
|
||||
<key>agent</key>
|
||||
<string>00bA05haB0d9ZC0fwGD09Q2OA30insbQ</string>
|
||||
<string>1B2h0ccl205c390Y28m1Ajdplkuu4wgy</string>
|
||||
<key>appversion</key>
|
||||
<string>44</string>
|
||||
<key>other</key>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// H5ErrorRelay.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// H5 错误中继:把 WebView 内 `console.error` / `window.onerror` /
|
||||
// `unhandledrejection` 通过 `webkit.messageHandlers.h5error` 上报到原生 print。
|
||||
// H5 日志/错误中继:把 WebView 内 `console.error` / `console.warn` /
|
||||
// `window.onerror` / `unhandledrejection` 通过 `webkit.messageHandlers.h5error`
|
||||
// 上报到原生 print;Debug 构建下额外转发 `console.log` / `console.info`。
|
||||
//
|
||||
// Phase 9.2(Design §11.2)。msext 时代仅靠用户/运营手工反馈;本项目作为
|
||||
// 原则 B「内部自由现代化」的一部分,开发期把 H5 异常打到 Xcode console,
|
||||
@@ -31,70 +32,127 @@ public final class H5ErrorRelay: NSObject {
|
||||
|
||||
/// 把 hook JS + message handler 注册到给定的 UserContentController。
|
||||
/// BridgedWebView.init 在挂 WVJB 之后调一次。
|
||||
public func install(into controller: WKUserContentController) {
|
||||
///
|
||||
/// - Parameter label: 该 WebView 的标识(大厅 / 子游戏),会作为输出前缀。
|
||||
/// 同时存在多个 BridgedWebView 时,没有前缀就分不清哪条日志来自谁,
|
||||
/// 也就无法反推该在 Safari「开发」菜单里选哪个条目。
|
||||
public func install(into controller: WKUserContentController, label: String) {
|
||||
let userScript = WKUserScript(
|
||||
source: Self.javaScriptSource,
|
||||
injectionTime: .atDocumentStart, // 业务 JS 之前 hook
|
||||
forMainFrameOnly: true // iframe 不抓
|
||||
)
|
||||
controller.addUserScript(userScript)
|
||||
controller.add(MessageProxy(target: self), name: Self.messageName)
|
||||
controller.add(MessageProxy(target: self, label: label), name: Self.messageName)
|
||||
}
|
||||
|
||||
fileprivate func handle(_ body: Any) {
|
||||
fileprivate func handle(_ body: Any, label: String) {
|
||||
guard let dict = body as? [String: Any],
|
||||
let kind = dict["kind"] as? String else { return }
|
||||
let tag = "[H5:\(label)"
|
||||
switch kind {
|
||||
case "console.log", "console.info":
|
||||
// 仅 Debug 构建会有这两类(见 javaScriptSource 的 #if DEBUG)。
|
||||
// ⚠️ 走裸 print 而不是 diagLog:H5 把 `Game_Config.Debugger.isDebugger`
|
||||
// 打开后每个收发包都会 console.log 一次,而 diagLog 会同步落盘到
|
||||
// `Documents/diag.log`(无轮转、无大小上限,见 BridgeBus.swift:47),
|
||||
// firehose 灌进去会把设备磁盘吃光。Xcode console 看得到就够了。
|
||||
let args = (dict["args"] as? [String]) ?? []
|
||||
print("\(tag) \(kind)] " + args.joined(separator: " "))
|
||||
case "console.error":
|
||||
let args = (dict["args"] as? [String]) ?? []
|
||||
diagLog("[H5 console.error] " + args.joined(separator: " "))
|
||||
diagLog("\(tag) console.error] " + args.joined(separator: " "))
|
||||
case "console.warn":
|
||||
// 桥诊断需要:WebViewJavascriptBridge.js 在「Native 发来的 handlerName
|
||||
// H5 侧没注册」时走 console.warn,之前不上报导致这类丢包完全不可见。
|
||||
let args = (dict["args"] as? [String]) ?? []
|
||||
diagLog("[H5 console.warn] " + args.joined(separator: " "))
|
||||
diagLog("\(tag) console.warn] " + args.joined(separator: " "))
|
||||
case "onerror":
|
||||
let msg = (dict["msg"] as? String) ?? ""
|
||||
let src = (dict["src"] as? String) ?? ""
|
||||
let line = (dict["line"] as? Int) ?? 0
|
||||
let col = (dict["col"] as? Int) ?? 0
|
||||
let stack = dict["stack"] as? String
|
||||
diagLog("[H5 onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? ""))
|
||||
diagLog("\(tag) onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? ""))
|
||||
case "resourceerror":
|
||||
// 资源 404 / 加载失败。H5 层完全静默(不会走 console.error),
|
||||
// 缺图 / 缺 js 在线上就是白屏或功能失灵,这里是唯一可观察点。
|
||||
let tagName = (dict["tag"] as? String) ?? "?"
|
||||
let src = (dict["src"] as? String) ?? ""
|
||||
diagLog("\(tag) resourceerror] <\(tagName)> 加载失败:\(src)")
|
||||
case "unhandledrejection":
|
||||
let reason = (dict["reason"] as? String) ?? ""
|
||||
let stack = dict["stack"] as? String
|
||||
diagLog("[H5 unhandledrejection] " + reason + (stack.map { "\n\($0)" } ?? ""))
|
||||
diagLog("\(tag) unhandledrejection] " + reason + (stack.map { "\n\($0)" } ?? ""))
|
||||
default:
|
||||
diagLog("[H5 unknown error] \(dict)")
|
||||
diagLog("\(tag) unknown] \(dict)")
|
||||
}
|
||||
}
|
||||
|
||||
/// JS 端 hook 三类异常源,统一通过 `webkit.messageHandlers.h5error.postMessage(payload)`
|
||||
/// JS 端 hook 各类日志/异常源,统一通过 `webkit.messageHandlers.h5error.postMessage(payload)`
|
||||
/// 上报。所有 send 调用都包 try/catch,hook 自身永远不应抛错(避免污染业务流程)。
|
||||
private static let javaScriptSource = """
|
||||
///
|
||||
/// `console.log` / `console.info` 只在 Debug 构建注入:Release 包不该为每条业务
|
||||
/// 日志付一次 JS→Native IPC,也不该把 H5 内部输出暴露给外部审阅。
|
||||
private static var javaScriptSource: String {
|
||||
#if DEBUG
|
||||
let verboseHooks = verboseConsoleHookJS
|
||||
#else
|
||||
let verboseHooks = ""
|
||||
#endif
|
||||
return """
|
||||
(function(){
|
||||
function send(payload){
|
||||
try { window.webkit.messageHandlers.h5error.postMessage(payload); } catch(e){}
|
||||
}
|
||||
// 参数格式化:直接 String(obj) 会得到无用的 "[object Object]",而大厅业务
|
||||
// 大量使用 console.log(msg) / console.log(res) 打整包,故对象走 JSON.stringify。
|
||||
// 循环引用时 stringify 抛错,回退 String();再抛就放弃,绝不让 hook 影响业务。
|
||||
function fmt(v){
|
||||
try {
|
||||
if (v === null) { return 'null'; }
|
||||
if (v === undefined) { return 'undefined'; }
|
||||
var t = typeof v;
|
||||
if (t === 'string') { return v; }
|
||||
if (t === 'number' || t === 'boolean' || t === 'function') { return String(v); }
|
||||
if (v instanceof Error) { return v.stack || (v.name + ': ' + v.message); }
|
||||
var s = JSON.stringify(v);
|
||||
return (s === undefined) ? String(v) : s;
|
||||
} catch(e) {
|
||||
try { return String(v); } catch(e2) { return '[unstringifiable]'; }
|
||||
}
|
||||
}
|
||||
function collect(a){
|
||||
var out = [];
|
||||
for (var i=0; i<a.length; i++) { out.push(fmt(a[i])); }
|
||||
return out;
|
||||
}
|
||||
var origErr = console.error;
|
||||
console.error = function(){
|
||||
try {
|
||||
var args = [];
|
||||
for (var i=0; i<arguments.length; i++) { args.push(String(arguments[i])); }
|
||||
send({ kind:'console.error', args: args });
|
||||
} catch(e){}
|
||||
try { send({ kind:'console.error', args: collect(arguments) }); } catch(e){}
|
||||
if (origErr) { origErr.apply(console, arguments); }
|
||||
};
|
||||
var origWarn = console.warn;
|
||||
console.warn = function(){
|
||||
try {
|
||||
var args = [];
|
||||
for (var i=0; i<arguments.length; i++) { args.push(String(arguments[i])); }
|
||||
send({ kind:'console.warn', args: args });
|
||||
} catch(e){}
|
||||
try { send({ kind:'console.warn', args: collect(arguments) }); } catch(e){}
|
||||
if (origWarn) { origWarn.apply(console, arguments); }
|
||||
};
|
||||
\(verboseHooks)
|
||||
// capture = true:资源加载失败(<script>/<img>/<audio> 的 404)只在元素自身
|
||||
// 触发 error 且**不冒泡**,不开捕获阶段就完全收不到 —— 而这恰恰是 H5 最常见的
|
||||
// 线上故障(缺图 / 缺 js)。JS 异常的 ev.target 是 window,据此分流两类。
|
||||
// 注:listener 挂在 window 上,window 自己是 target 时走 AT_TARGET 阶段,
|
||||
// 不受 capture 标记影响,所以一个 listener 同时收得到两类。
|
||||
window.addEventListener('error', function(ev){
|
||||
var t = ev.target;
|
||||
if (t && t !== window && t.tagName) {
|
||||
send({
|
||||
kind: 'resourceerror',
|
||||
tag: String(t.tagName),
|
||||
src: String(t.src || t.href || '')
|
||||
});
|
||||
return;
|
||||
}
|
||||
send({
|
||||
kind:'onerror',
|
||||
msg: String(ev.message || ''),
|
||||
@@ -103,7 +161,7 @@ public final class H5ErrorRelay: NSObject {
|
||||
col: ev.colno || 0,
|
||||
stack: (ev.error && ev.error.stack) ? String(ev.error.stack) : null
|
||||
});
|
||||
});
|
||||
}, true);
|
||||
window.addEventListener('unhandledrejection', function(ev){
|
||||
var r = ev.reason;
|
||||
send({
|
||||
@@ -114,6 +172,27 @@ public final class H5ErrorRelay: NSObject {
|
||||
});
|
||||
})();
|
||||
"""
|
||||
}
|
||||
|
||||
/// Debug 专用:`console.log` / `console.info` 全量转发。
|
||||
///
|
||||
/// 注意 H5 自带调试总开关 `Game_Config.Debugger.isDebugger`
|
||||
/// (`gamehall.zip` → `js/01_SubGame/00_SubGame_Config.js`)出厂为 `false`,
|
||||
/// 收发包日志(`发送数据:` / `接收数据:`)被它挡住。要看这些需在
|
||||
/// Safari Web Inspector 控制台运行时打开:`Game_Config.Debugger.isDebugger = true`
|
||||
/// —— 运行时改内存属性,不动 H5 任何文件(契约原则 A)。
|
||||
private static let verboseConsoleHookJS = """
|
||||
var origLog = console.log;
|
||||
console.log = function(){
|
||||
try { send({ kind:'console.log', args: collect(arguments) }); } catch(e){}
|
||||
if (origLog) { origLog.apply(console, arguments); }
|
||||
};
|
||||
var origInfo = console.info;
|
||||
console.info = function(){
|
||||
try { send({ kind:'console.info', args: collect(arguments) }); } catch(e){}
|
||||
if (origInfo) { origInfo.apply(console, arguments); }
|
||||
};
|
||||
"""
|
||||
}
|
||||
|
||||
// MARK: - WKScriptMessageHandler proxy(weak target 切断 retain cycle)
|
||||
@@ -121,9 +200,11 @@ public final class H5ErrorRelay: NSObject {
|
||||
@MainActor
|
||||
private final class MessageProxy: NSObject, WKScriptMessageHandler {
|
||||
private weak var target: H5ErrorRelay?
|
||||
private let label: String
|
||||
|
||||
init(target: H5ErrorRelay) {
|
||||
init(target: H5ErrorRelay, label: String) {
|
||||
self.target = target
|
||||
self.label = label
|
||||
super.init()
|
||||
}
|
||||
|
||||
@@ -131,6 +212,6 @@ private final class MessageProxy: NSObject, WKScriptMessageHandler {
|
||||
didReceive message: WKScriptMessage) {
|
||||
// WKScriptMessageHandler 在主队列派发;MessageProxy / H5ErrorRelay 都
|
||||
// MainActor,直接调即可,无需 Task 切换。
|
||||
target?.handle(message.body)
|
||||
target?.handle(message.body, label: label)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public final class BridgedWebView: UIView {
|
||||
// ── H5 错误中继(Phase 9.2,原则 A 零修改 H5)─────────────
|
||||
// 把 console.error / window.onerror / unhandledrejection 通过独立
|
||||
// 的 webkit.messageHandlers.h5error 桥到原生 print,开发期减少盲点。
|
||||
H5ErrorRelay.shared.install(into: configuration.userContentController)
|
||||
H5ErrorRelay.shared.install(into: configuration.userContentController, label: label)
|
||||
|
||||
// ── 创建 WKWebView + BridgeBus ─────────────────────────
|
||||
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
|
||||
@@ -412,5 +412,26 @@ extension SubGameViewController: WKNavigationDelegate {
|
||||
self?.splash.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
// ── 加载失败 / WebContent 进程崩溃:仅记录,不改行为 ──────────────
|
||||
// 说明同 WebContainerViewController 对应实现。子游戏更容易踩到:zip 下载不全 /
|
||||
// 解压残缺 / gameStart 目录名对不上时都会走 didFailProvisionalNavigation。
|
||||
public func webView(_ webView: WKWebView,
|
||||
didFailProvisionalNavigation navigation: WKNavigation!,
|
||||
withError error: Error) {
|
||||
let ns = error as NSError
|
||||
diagLog("[WebView:subGame] 首次加载失败 code=\(ns.code) \(ns.localizedDescription)")
|
||||
}
|
||||
|
||||
public func webView(_ webView: WKWebView,
|
||||
didFail navigation: WKNavigation!,
|
||||
withError error: Error) {
|
||||
let ns = error as NSError
|
||||
diagLog("[WebView:subGame] 提交后加载失败 code=\(ns.code) \(ns.localizedDescription)")
|
||||
}
|
||||
|
||||
public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
||||
diagLog("[WebView:subGame] ⚠️ WebContent 进程终止(多为内存不足),页面已白屏")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -760,5 +760,30 @@ extension WebContainerViewController: WKNavigationDelegate {
|
||||
self?.splash.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
// ── 加载失败 / WebContent 进程崩溃:仅记录,不改行为 ──────────────
|
||||
// 此前这三个回调都没实现:H5 加载不出来(AppSchemeHandler 找不到 index.html、
|
||||
// zip 解压残缺)时 splash 永不淡出、卡在启动图,Xcode 里一行日志都没有;
|
||||
// WebContent 进程被系统回收(canvas 游戏 JSC OOM)时页面直接白屏且同样静默。
|
||||
// 这些都不是 JS 异常,H5ErrorRelay 抓不到,只能在原生导航层观察。
|
||||
// ⚠️ 只 log 不自动 reload —— 自动恢复是行为变化,msext 没有,不引入。
|
||||
public func webView(_ webView: WKWebView,
|
||||
didFailProvisionalNavigation navigation: WKNavigation!,
|
||||
withError error: Error) {
|
||||
let ns = error as NSError
|
||||
// -999 = NSURLErrorCancelled,多为后续导航覆盖前一次,属正常噪声
|
||||
diagLog("[WebView:lobby] 首次加载失败 code=\(ns.code) \(ns.localizedDescription)")
|
||||
}
|
||||
|
||||
public func webView(_ webView: WKWebView,
|
||||
didFail navigation: WKNavigation!,
|
||||
withError error: Error) {
|
||||
let ns = error as NSError
|
||||
diagLog("[WebView:lobby] 提交后加载失败 code=\(ns.code) \(ns.localizedDescription)")
|
||||
}
|
||||
|
||||
public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
||||
diagLog("[WebView:lobby] ⚠️ WebContent 进程终止(多为内存不足),页面已白屏")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user