Files
joywayerandClaude Opus 4.7 800497fd18 新增 H5-Debug-Guide:Safari Web Inspector 调试 H5 控制台的操作手册
整理大厅 / 子游戏 / 弹层三处 WKWebView 的 isInspectable 启用条件、
Mac Safari 与真机端开关、多 WebView 排查思路(Develop 菜单不刷新等
常见坑),并在 CLAUDE.md 文档索引登记。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-24 21:37:09 +08:00

316 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# daoqi 项目协作约定(面向 Claude / AI 助手)
> 本文件随 daoqi 仓库分发,任意设备 clone 后均按此约定执行。
---
## 适用范围声明
**本 CLAUDE.md 仅适用于 daoqi 项目自身**
- 父目录(任何上级目录)下可能存在其它 CLAUDE.md 文件,那些文件**不是为本项目编写**的,与 daoqi 项目无关,**不参照、不继承、不引用**。
- Claude / AI 助手在本项目工作时,只读取并遵循本文件(`./CLAUDE.md`)与本项目 `docs/` 下的文档,不向上递归引用父级 CLAUDE.md。
- 如果在对话上下文中出现父级 CLAUDE.md 的内容(由工具自动加载),应当忽略,以本文件为唯一权威。
---
## 项目内容
daoqi 仓库当前并存两条工作线:
1. **现有 iOS 外壳(`msext` target** — 已上线版本。Objective-CMRC,未启用 ARC),最低 iOS 9.0;原生外壳 + 内置 H5`gamehall.zip` 解压到沙盒)+ WebViewJavascriptBridge 桥接。
2. **新外壳设计(greenfield 重写)** — 计划中的下一代版本。设计原则、接口契约、实施蓝图见 `docs/` 下两份文档。
---
## 原项目 daoqi:遇到问题时的参考来源
**原 daoqi 项目代码位于** `/Users/joywayer/Documents/Works/YouleApps/iOS/youle_app_ios/daoqi/`(与 ylgamehall 同级目录)。该项目已稳定运行多年(msext target),是同一 H5 业务、同一渠道分发模式、同一启动流程的"工作参照实现"。
### 何时主动去读原项目
新项目实施过程中**遇到任何一类下述问题,第一动作是去 daoqi 找参照**,而不是凭推测搭方案:
- **iOS 行为不符合预期 / 与文档描述有差异**(如本次的 LaunchScreen orientation 问题:iOS 26 + Xcode 26 默认 Build Settings 写法导致启动图被 portrait 渲染再旋转 90°)
- **某个 Info.plist key / Build Settings 不确定该不该加 / 加哪种变体**
- **某个 H5 桥接 handler 的返回值结构 / 字段命名 / 类型不确定**
- **某个三方 SDK 的初始化时序 / 回调链疑似有顺序依赖**
- **渠道注入 / 启动流水线 / 升级逻辑某一步与设计文档对不齐**
- **某个看似可改的"老 hack"想清掉,但不确定该 hack 是否解决过某个具体问题**
### 怎么用原项目
1. **先查关键文件**`daoqi/msext/Info.plist``daoqi/msext/Class/AppDelegate.{h,m}``daoqi/msext/Class/UI/NewRootVC.m`(启动流水线全部在此)、`daoqi/Podfile`(看用了什么三方)
2. **grep 同名 key / 同名 handler**:例如 `grep -r "UISupportedInterfaceOrientations" daoqi/msext/Info.plist`
3. **看 git log / git blame**:原项目积累的"看似古怪的写法"通常都有当年的修复故事
4. **对照差异**:原项目稳定运行的写法是事实基准;新项目偏离它的部分必须有明确的"为什么改"的理由(通常出现在 `docs/H5-Native-Implementation-Design.md` 的 ADR 或 §6.3.6 等差异表里)
### 与「原则 B:内部自由重构」不冲突
- 参考原项目 ≠ 照抄原项目的实现细节
- 参考原项目 = 把它当作 **"哪些事必须做"的清单** 和 **"哪些坑已经踩过"的备忘**
- 新项目的现代化重构(Swift 6 / SwiftUI / actor 隔离 / SPM 依赖等)依然按原则 B 自由设计;只是设计前先确认"我没有漏掉原项目实际需要解决的事"
### 典型案例:LaunchScreen orientation(启动图方向错误)
- **症状**:新项目启动图渲染成竖向矩形再被整体旋转 90°,画面横躺在中间
- **错误做法**:凭 Xcode 26 文档推测、纯调整 storyboard / contentMode / 反复换素材
- **去原项目找答案**`daoqi/msext/Info.plist` + `msext.xcodeproj/project.pbxproj`
- 发现 1Info.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 **完全不必要**
- 发现 3Contract §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 不可妥协)
- **症状**H5 端读到 `app_gameconfig` 等关键变量是 H5 zip 包内默认占位值,不是 ChannelConfig.plist 的实际渠道值
- **调研发现**(按 CLAUDE.md「先看原项目」规则):原 msext 在 `NewRootVC.viewDidLoad → initJSdata``loadFileURL` **之前**)用 `writeToFile:``app_data.js` 写入沙盒,H5 启动时通过 `<script src>` 同步引入就能直接读到实际值。新外壳改用现代化的 `WKUserScript(.atDocumentEnd)` + `evaluateJavaScript` 路径,**时序无法 1:1 等价**:
- `documentStart` 注入会被 H5 自带 `app_*.js` 内的 `var app_xxx = "默认值"` 声明覆盖回退
- `documentEnd` 注入又太晚(H5 业务顶层 `<script>` 在 documentEnd 之前已经跑过 `var x = app_xxx`,读到的是默认占位)
- WKUserScript 没有"在 `<script src="app_data.js">` 之后、业务 `<script>` 之前"的精确时机
- **错误做法**:提出 3 个方案,其中 A "H5 改一行:把 `app_xxx` 用法移到 `DOMContentLoaded` 之后"和 B "H5 改 4 个文件:用 `if(typeof xxx==='undefined')` 守卫" 都要求 H5 配合改动 — **违反原则 A 第一准则**,立即否决
- **正确做法**:方案 C — 原生 Swift 端**回到 msext 老路**,在 `loadFileURL` 前用 `String.write(to:)``app_*.js` 物理文件写到沙盒,覆盖 H5 zip 包内自带的版本。哪怕这是"现代 WKWebView 不推荐"的写法,但行为 100% 等价 msext,契约不破
- **教训**
- **遇到契约边界时不要琢磨"H5 改一点点就行"**——再小的改动都不行,第一准则不可妥协
- **`WKUserScript` / `evaluateJavaScript` 不是万能药**:现代化 API 在某些时序场景下**无法**1:1 等价旧路径,遇到时序冲突就回到旧路径,不要为了"看起来现代"而牺牲契约
- **原则 B "现代化"对原则 A 让步**:Swift 写文件 / 看似过时的模式在新外壳里完全可以存在;不优雅是可接受的代价,契约破裂不是
---
## 文档索引
| 文档 | 角色 | 何时读 |
|------|------|--------|
| `CLAUDE.md`(本文件) | 项目协作约定、契约边界声明 | 每次对话先读 |
| `docs/H5-Native-Contract.md` | H5 ↔ 原生 桥接**契约**(黑盒可观察行为) | 涉及桥接接口必读 |
| `docs/H5-Native-Implementation-Design.md` | 新外壳实施蓝图(架构、模块、代码骨架) | 实现新外壳必读 |
| `docs/Development-Plan.md` | 开发执行计划(Phase 0–10、依赖图、进度追踪) | 排期 / 进度更新必读 |
| `docs/Verification-Checklist.md` | 功能验证清单(开发期累积、统一验收的操作手册) | 每完成 Phase 子项时补充 / 勾选;不要在 Phase 进行中频繁跑,等多个 Phase 累积后统一验证 |
| `docs/SDK-Integration-Guide.md` | 微信 / 高德定位 / opencore-amr / 七牛 接入操作手册(凭证 / Vendor / Info.plist / 代码触点 / BackGameData 清理 / 验收)| 项目方发 SDK 凭证 + 二进制后,按对应章节落地;接完回 §5 清单核对 |
| `docs/H5-Debug-Guide.md` | H5 调试方法(Safari Web Inspector 看大厅 / 子游戏 / 弹层网页输出的操作手册) | 调试 H5 端 console / Network / DOM / 断点时;新增 WKWebView 时也按其 §2 补 `isInspectable` 守卫 |
| `../daoqi/`(仓库同级目录) | 原项目 msext 工作参照实现 | 遇到 iOS 行为差异 / 渠道注入 / 桥接 / SDK 初始化等不确定问题时,**优先参考此处**而非凭推测,详见下文「原项目 daoqi:遇到问题时的参考来源」 |
---
## 依赖管理约定
新项目采用**纯 SPM + Vendor `.xcframework` 两层管理****禁止引入 CocoaPods**(详见 `docs/Development-Plan.md` ADR-006)。
- **首选 SPM**:开源 / 官方提供 Swift Package 的库(Sentry / ZIPFoundation / 七牛 `https://github.com/qiniu/objc-sdk` v8.9.x 等)
- **Vendor `.xcframework` 手动**:所有闭源二进制(微信 / 高德 AMap / opencore-amr 等),按 Design §14.1「Vendor 接入标准流程」放 `Vendor/<SDKName>/` + Target → General → Frameworks 加入 + `Vendor/<SDKName>/README.md` 记录版本与来源
- **不接入 QQ SDK / 也不接入抖音 SDK**H5 调 `friendsShare(sharefriend=1, ...)` 时原生弹 SharePanel 三选一(微信好友 / QQ / 抖音,参 msext `gameController.m:585`)。QQ 走 URL Scheme`mqqapi://share/to_fri?...`,参 msext `QQShareManager.m:14` `__has_include` fallback),抖音走 URL Scheme + Photos`snssdk1128://share/video` + 保存截图到相册让用户在抖音里选,参 msext `DouyinShareManager.m`)。两者都无 SDK 依赖,Info.plist 仅需 `LSApplicationQueriesSchemes``mqq` / `mqqapi` / `mqqopensdkfriend` / `snssdk1128`+ `NSPhotoLibraryAddUsageDescription`(抖音需要写入相册)
- **禁止 CocoaPods**:项目内不放 `Podfile` / `Podfile.lock` / `Pods/` / `.xcworkspace`。工程入口固定为 `ylgamehall.xcodeproj`
- **新增第三方依赖时**:按 SPM → Vendor 顺序尝试,PR 描述写明选型理由;若发现某 SDK 既无 SPM 又无 XCFramework,回到 ADR-006 决策框架重新评估
---
## docs/res/ 与项目资源的关系
`docs/res/` 是**项目维护者的私人原始素材池**,**与项目架构无关**:
- 仅作为开发期可能用到的素材的随手存放点(H5 团队提供的 zip / 美术给的 png / 原 msext 沉淀的 mp3 / 历史 Asset Catalog 等)
- **项目代码 / 构建脚本 / Xcode 工程都不感知它的存在**,不依赖它的目录结构,不引用它的任何文件
- 内容、结构、命名随时可由维护者调整,不影响构建
- git 跟踪它只是为了多机同步素材,而不是因为项目需要它
### 项目自己的资源目录由项目独立规划
**禁止把 `docs/res/` 当作项目的资源目录使用**(即不要在代码 / 脚本 / Build Phase 中直接引用 `docs/res/xxx`)。当某项工作需要某个素材时:
1.`docs/res/` **拷贝一份**到项目按现代 iOS 实践规划的目标目录(如 `ylgamehall/Resources/``ylgamehall/Assets.xcassets/``Vendor/<SDK>/` 等)
2. 由 Xcode 工程结构 / synchronized group / Build Phase 接管该素材的打包流程
3. 后续维护与 `docs/res/` 不再有任何关联——`docs/res/` 那份是"原始备份",工程内那份是"在用版本"
**约定**:项目实际需要的资源目录结构由 Design / Plan 在各 Phase 实施时按需定义并落到工程内;不预先把"所有未来可能用到的资源"硬塞进某个固定 Resources 目录。
---
## 核心约定:两条原则(原则 A 是第一准则,地位高于原则 B)
> 早期版本曾把 A / B 描述为"地位对等"**2026-06-22 升级**:原则 A 是**第一准则**,地位**高于**原则 B。两者冲突时无条件选 A、压力让原生 Swift 端承担。
### 原则 A:H5 端零修改(第一准则,不可妥协)
**H5 端不做任何改动**——一行代码、一个字符、一个文件名、一个调用时机,都不允许 H5 团队配合调整。这是新外壳所有设计 / 实现的**最高约束**,地位高于原则 B、高于"技术优雅"、高于"现代化"、高于任何关于"性能 / 可维护性 / 可观察性"的考量。
#### A.1 已经被否决的 / 永远不要再提出的方案
下面这些方案在任何场景下都**不要**作为建议提给项目方,即便它们更简洁 / 更现代 / 工作量更小:
- ❌ "H5 改一行":哪怕只让 H5 把读 `app_xxx` 的代码挪到 `DOMContentLoaded` 之后
- ❌ "H5 改一个文件":哪怕只让 H5 把 `app_*.js` 内容换成 `if(typeof xxx==='undefined') var xxx=...`
- ❌ "H5 加一个 polyfill":哪怕只让 H5 引入一段 5 行的兼容垫片
- ❌ "H5 调整调用时机":哪怕只让 H5 把同步表达式改成 `setTimeout(..., 0)`
- ❌ "新桥接口":哪怕只让 H5 改成调一个新 handler 名
**唯一例外**:项目方主动要求改 H5 时(如 H5 团队自己提出重构),新外壳配合调整契约。其它一切场景下,H5 不可触碰。
#### A.2 替代方案:原生 Swift 端硬扛
遇到契约边界冲突时正确的应对:
1. **回到原 msext 老路**:原 msext 是上线 N 年的稳定实现,它怎么解决就照搬。哪怕老路是"现代 WKWebView 不推荐"的写法(如 `loadFileURL``writeToFile` 写 .js 文件、`UIWebView` 时代的 `JSContext` 注入约定),只要功能等价,照搬
2. **接受技术不优雅**:硬扛会让原生 Swift 代码出现"看似过时的写法"(写文件、轮询、特殊时机 hook 等)。**这是可以接受的**——原则 B "现代化"对原则 A 让步,不是相反
3. **不要用"虽然这是临时方案,等 H5 改了再优雅化"自我安慰**H5 永远不会"等会儿改"。**今天的临时方案就是终身方案**
#### A.3 判定流程
写代码 / 写文档前问自己:
> "这个方案要 H5 端做任何动作吗?包括 H5 团队改一行、调整调用顺序、删一个文件、加一个守卫?"
>
> - 答"是" → **立即否决,重新设计**
> - 答"否" → 继续判定其它原则
### 原则 B:原生内部实现自由重构(内部架构)
**原生内部的实现可以也应当与原项目不一致**。新外壳不是旧 msext 的复刻,是按现代标准重新设计的产物,应当主动追求:
- **架构优雅** — 模块边界清晰、依赖单向、单一职责;不再受旧项目 PCH 全局注入、MRC、UIWebView、JSContext 等历史包袱约束
- **高效 / 高性能** — 启动并发优化、WKWebView ProcessPool 复用、桥消息批处理、actor 隔离的并发模型;性能指标可度量、有 SLO
- **专业** — 完整的单测覆盖、CI 流水线、代码规约、文档体系;不留"等会再补"的口子
- **成熟** — 选用社区主流、长期维护的库(URLSession、SPM、ZIPFoundation、async/await);摒弃 ASIHttpRequest、SBJSON、UIWebView、AFNetworking 等已废弃技术
→ 详细技术选型与架构设计见 `docs/H5-Native-Implementation-Design.md`
**两条原则的交叉判定**A 是第一准则):
- 如果某项决策影响 H5 可观察行为(接口名/字段/数据/JS 协议/时序)→ 必须遵循原则 A,**照搬原 msext 实现,哪怕看起来过时**
- 如果某项决策**只**影响原生内部(技术栈/模块结构/状态管理/SDK 选型/代码风格)→ 必须遵循原则 B,按现代标准设计,不要照搬旧项目
- 当两者冲突时,**无条件选 A**:原则 A 是不可妥协的第一准则。原生内部多一层适配层 / 多一段"看似过时"的代码 / 多一份与原 msext 等价的实现,都可以接受;契约被破,**永远不可以接受**
---
## 原则 A 详述:H5 适配的五类必须一致
为达到 H5 端零修改适配,以下五类内容**必须 1:1 与现网一致**:
### 1. 桥接接口名(handler 名字符串)
H5 端通过字符串名调用原生 handler,例如 `bridge.callHandler('mediaTypeAudio', ...)``bridge.callHandler('friendsSharetypeUrlToptitleDescript', ...)`
- **新外壳注册的 handler 名必须字面一致**,少一个字符、改一个大小写、改一个驼峰都视为契约违反
- 含历史包袱命名照搬:`SwitchOverGameData`(驼峰首字母大写)/ `friendsSharetypeUrlToptitleDescript`(长形式)/ `gameui_play_voice`(下划线)/ `OpenurlTitleData`
- 完整 39 项 handler / callback 名见 `docs/H5-Native-Contract.md` §3
### 2. 参数字段名
H5 → Native 时 data 字典中的 key,以及 Native → H5 callback payload 中的 key。
- **字段名字面一致**,含历史包袱:
- `sharelogin` 回包中 `Province` 是**大写 P**(不是 `province`
- `OpenurlTitleData` 入参中 `"title "` 末尾**有一个空格**(不是 `"title"`
- `getphoneinfo` 回包用**小写 i**(H5 调用方是 `getphoneInfo` 大写 I
- `appservice` 用字符串 `"2"`(后台)/ `"1"`(前台),不是数字 ⚠️ 值错位沿用 msext WKWebView 路径历史,参 `docs/H5-Native-Contract.md` §3.2 `appservice` 值错位说明
- `getlocationinfo``latitude` / `longitude`**string** 而不是 number`stringWithFormat:@"%f"` 产生)
### 3. 参数数据结构
- H5 → Native 入参类型(string / number / bool / object / array
- Native → H5 回包结构(每个字段的类型、嵌套层次)
- 字段是否可缺、缺时如何 fallback
→ 完整结构见 `docs/H5-Native-Contract.md` §3.1 / §3.2 表 A/B/C
### 4. H5 端注册方式(JS 协议)
H5 页面通过特定的 JavaScript 协议初始化桥并注册自己的 handler。**新外壳必须使用 H5 端代码一致的 JS 协议**:
- **大厅 / 子游戏页**:H5 使用 `WebViewJavascriptBridge` 的 JS 协议(`_setupWebViewJavascriptBridge` 握手、`bridge.callHandler(...)``bridge.registerHandler(...)`)。新外壳实现必须支持这套握手协议(推荐做法:直接复用 marcuswestin/WebViewJavascriptBridge 的 JS 端源码,原生侧自实现)
- **弹层(threeView 等价容器)页**H5 使用 `window.settings.xxx(...)` 直接同步调用形式(JSExport 协议)。新外壳如改用 WKWebView,需注入 polyfill 让 `window.settings.{backgameData, browser, finishweb}` 仍然按相同形式可用
任何打破 H5 端注册方式的实现(如换用自创 JS 协议、改变 `window.settings` 名字)都视为契约违反。
### 5. 空实现接口也必须保留注册
当前现网中 `opensaoma``getGameplay` 等 handler 是空实现(`responseCallback("opensaoma")` 后无副作用),但 H5 仍然会调用。
- **新外壳必须以同名空 handler 注册**,否则 H5 调用时触发 "bridge handler not found" 错误分支,导致业务流程中断
- 完整空实现清单见 `docs/H5-Native-Contract.md` §3.1
---
## 不属于契约的部分(自由实现)
除上述五类外,原生内部一切自由:
- 技术栈(Swift / Objective-C / SwiftUI / UIKit 任选)
- 桥接库选型(自实现 WKScriptMessageHandler / 引用 WebViewJavascriptBridge 都行)
- 网络层(URLSession / Alamofire / AFNetworking
- 状态管理、并发模型、命名风格、SDK 选型
- 启动时序内部并发优化、解压实现、零拷贝优化
- NSNotification 内部模块通讯命名(不是 H5 可见)
- 监控、日志、CI 选型
**判断某项是不是契约的方法**:问"H5 代码能不能感知到?"。能感知 → 契约;不能感知 → 自由。
---
## 验收原则
新外壳完成后,**在不修改 H5 任何一行代码**的前提下,逐项跑 `docs/H5-Native-Contract.md` §10 的 26 项验收清单。任意一项 H5 表现与现网不一致即视为契约违反,需要排查;表现一致即视为通过,原生内部实现自由。
---
## 提交规范
- 提交信息使用中文短句,描述"做了什么 + 为什么"
- 不在未授权情况下执行 `git push``git reset --hard``git rebase``git push --force` 等不可逆操作
- 涉及桥接接口名 / 参数字段名 / 数据结构的改动,提交信息必须明示 "契约影响" 并附 `docs/H5-Native-Contract.md` 对应章节
- **及时提交,无需每次确认**:改动通过最小验证(BuildProject 通过 / 关键路径手测通过 / 单测通过)后,可直接 `git commit`,不必每次先征求用户同意。
- **commit 颗粒按情况自行判断**
- 多个改动彼此相关、属于同一目标的,可合并为一个 commit
- 多个改动彼此完全无关、合并后 git diff 失去可读性的,再拆开
- 桥接契约改动 / 不可逆的结构性变更必须独立 commit,便于回滚定位
- 不强制"按职责拆"——合并 vs 拆分以"未来回看时能不能看懂、能不能二分回滚"为准
- commit message 直接写"做了什么 + 为什么",无需事先把建议 message 给用户审一遍。用户若不满意会主动反馈,再 amend / 重提即可
---
## 进度同步规范
**每完成一个 Phase 子项(如 1.10 / 1.11 / 1.12 ...)必须立即更新 `docs/Development-Plan.md` 的进度标记**,不允许"代码提交但 Plan 未同步"的滞后。具体:
- Plan §5 各 Phase 任务清单:完成的子项把 `- [ ]``- [x]`,附简短结果或耗时
- Plan §8 进度追踪 checklist:同步勾选 + 简短补充(如算法名 / 端到端测试结果)
- 时机:通常与该子项的 `git commit` 同一 commit 内完成,commit message 末尾备注"Plan 进度已勾选"
- 若改动跨多个 Phase 子项(极少见),逐项勾选不能漏
- 当任务清单中的描述与最终实现有差异(如新增子任务、合并子任务、改变实现策略)时,**同步修改任务描述**,不允许 Plan 文档与代码长期不一致
理由:
- Plan 是项目记忆的唯一权威进度视图,新人 clone 后只看 Plan §8 就能立刻知道整体进度
- 若代码已完成但 Plan 未勾选,新人会误判项目状态、做重复工作
- 若 Plan 描述与代码实现不一致,未来 Phase 验收 / 回归测试时会基于错误前提
- 与 commit message 双写互为校验:commit log 是细节,Plan 是全局视角