Files
youle_framework/docs/client/development-guide/05-开发规范与红线.md
T
2026-08-19 08:14:48 +08:00

176 lines
17 KiB
Markdown
Raw 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.
# 05 · 开发规范与红线
本篇汇总前端**必须遵守**的工程纪律,是代码审查清单的来源。改任何代码前按相关条目自检。前面 01–04 是“怎么做”,本篇是“不许怎么做 / 必须怎么做”。
---
## 1. 可编辑范围
| 区域 | 可改性 |
|------|--------|
| `js/vendor/`、`js/00_Surface/` | **禁改**(第三方/平台代码) |
| `01_SubGame/00_SubGame_Config.js` | **仅可改值**:只给**已有** `Game_Config.*` 配置项赋值;**禁止**新增/删除/改名/改结构定义(三文件中唯一子游戏可碰者,且仅限改值)。详见 [06](./06-子游戏接入模式与Hooks外置.md) |
| `01_SubGame/01_SubGame_modify.js` | **受限**:仅顶部「配置区」填值(`Type_*/CreateRoomData/combat/game_config/roomDes`);转发壳/平台接口**不碰、不新增**,业务走子游戏实现层。详见 06 |
| `01_SubGame/02_SubGame_Input.js` | **不碰**:框架维护的平台接口骨架/转发壳,不改、不新增接口、不写业务。详见 06 |
| `js/gameabc-framework/` | 可改,但须保持**游戏中立**(见 §3) |
| `01_SubGame/codes/`(除 `shared/`) | 子游戏自由开发区 |
| `01_SubGame/codes/shared/` | **只读**:服务端 `shared/` 的同步副本,改服务端权威源再同步(见 §7) |
新增前后端交互一律走服务端 `mod.js` 的 `rpc` 机制 + 前端统一发包/收包封装,**不在受限文件新增接口**。
---
## 2. 语言标准:严格 ES5
- 用 `var`/`function`;对象继承用 `Object.create(Base)` + 在 `init` 内重声明实例属性。
- **禁** `let`/`const`、箭头函数、模板字符串、解构、默认参数、展开、`class`、`for...of`。
- 跨文件按**全局名**引用(各文件 `var X = ...` 暴露全局,`index.html` 顺序加载);新增文件必须在 `index.html` 插到依赖之后、使用者之前。
---
## 3. 框架游戏中立
`gameabc-framework/` 是给**任意子游戏**复用的通用框架,**不得**渗入任何具体玩法:
- **禁止**在框架内出现玩法逻辑、玩法专属常量(牌型、动作、玩法事件名等)。
- 玩法专属内容一律定义在**子游戏侧**:
- 玩法事件 → 子游戏事件常量文件,追加到 `EventBus.Events`;
- 音效映射 → 子游戏音效资源常量 + 游戏音频管理;
- Spine 动作 → 子游戏 Spine 动作配置;
- 资源/布局 → 子游戏精灵结构/图片资源/布局常量。
- **违例信号**:框架文件里出现 `mahjong:`、牌型、吃碰杠胡等具体玩法字样(注释举例除外,且举例应尽量中立)。
> 范例:框架 `EventBus.js` 预定义事件常量全部清空(只留 `EventBus.Events={}`),事件改由子游戏事件常量文件定义;通用精灵事件控制器抽到框架 `system/SpriteEventController.js` 并把玩法标记耦合改为「全局 draw 钩子」;`UIManager` 的全局 UI(Loading/Message/Confirm)由子游戏 `init(config)` 注入而非主动读子游戏常量。新玩法照此扩展,框架零改动。
---
## 4. 渲染与常量
- **精灵只走 `SpriteManager`**:UI 代码禁止直接调 `GameABCUtils`/引擎原生 API。
- **ID 守范围**:精灵 1001–3000、群组 ≥201、普通图层 101–200、弹窗图层 301–400(另有 501–600、701+ 备用段);框架保留 1–1000 及 3001+(精灵)等交替段,不可占用;ID 必须与编辑器一致,不编造。
- **检查返回值**:`SpriteManager.*` 返回 `false` 即 ID/范围有误,及时暴露。
- **常量集中、禁硬编码**:UI 代码**禁止出现任何硬编码的 id 或裸值**——精灵 ID / 群组 ID / 图层 ID / 图片资源 ID / 声音 ID / Spine 资源 / 坐标尺寸 / 动画时长帧数 / 事件名,一律定义在对应常量文件,业务代码**只引用常量**:
- 精灵结构(含群组 / 图层 ID)→ 精灵结构常量(主界面/弹窗分文件)
- 图片资源 → 图片资源常量
- 布局坐标 → 布局常量
- 动画参数 → 动画配置
- 音效 / 语音 ID → 音效资源常量
- Spine 资源 → Spine 动作配置
- 事件名 → `EventBus.Events`(专属在子游戏事件常量文件)
- 整合入口 → 精灵常量整合入口(**最后加载**)
---
## 5. UI 组件生命周期与内存安全
- **组件继承 `BaseComponent`**,在 `init` 内重声明实例属性、建精灵、注册事件。
- **事件用 `this.addEventListener`** 订阅(`destroy()` 自动 `off`),**禁**裸 `EventBus.on`(会泄漏)。
- **销毁用 `destroy()`** 而非只 `hide()`;在 `onDestroy` 清理**定时器、动态复制精灵、Spine 回调登记**。
- 动态列表用 `DynamicSpriteList` 并在销毁时 `list.destroy()`,避免复制精灵残留。
---
## 6. 数据驱动架构:服务端状态的投影
**前端是服务端对局状态的一个投影(view),不是状态的第二个来源。** 阶段、轮次、控制权、可用操作、倒计时、分数、按钮可用性……一切对局态都由服务端唯一维护并随包下发,前端只做「**读包字段 → 写 `this.data` → 据 `this.data` 画界面**」这一条链路。这既是正确性要求(两端各推一套必然分叉),也是**反作弊**要求:**前端能自己推出来的东西,就是玩家能改的东西**。
- **服务端权威**:所有核心运算与胜负裁定以服务端为准、数据以服务端为权威;前端只做界面展示与玩家交互,本地数据仅为「渲染副本」,不做权威计算。
### 6.1 前端无对局状态机
- **状态机唯一在服务端**:阶段(`phase`)、当前控制权(轮到谁)、该座位可用操作(`availableActions`)、倒计时剩余、比分/结算——一律**读服务端下发的权威字段**渲染。
- **禁止本地推导**:不得由「上一个包 + 本地规则」推出「现在该轮到谁 / 现在进入哪个阶段 / 现在该显示哪几个按钮」。缺字段是**服务端漏发**,修在服务端发包处(见服务端 [03 §1.2](../../server/development-guide/03-数据收发与通信协议.md)),**前端不推、不补、不猜**。
- **禁止前端推进流程**:不得用本地定时器自动推进阶段、自行结算、超时后自判胜负或自动补发包。**超时/托管一律由服务端驱动**,前端只显示服务端下发的结果。
- **倒计时的正确形态**:服务端下发锚点/剩余时长,前端可本地 tick 做插值显示;但**归零不代表状态改变**——超时的裁定与后续推进仍等服务端推送。
- **`shared/` 预判不是状态**:前端跑 `shared/` 算出的胡牌/听牌/合法性只用于**提示与预校验**(如置灰不可点的牌),**不得**据其改写对局态、也**不得**用来替代服务端下发的 `availableActions`(见 §8)。
### 6.2 视图 = f(服务端快照)
- `this.data` 是**服务端状态的镜像**,不是第二份真相;**不得存在「只活在前端、服务端不知道」的对局态**。
- **判据(可直接用于自检与代码审查)**:任意时刻丢弃全部 `this.data`,仅用**最近一次服务端快照**(重连 `get_deskinfo` 或最近一次推送)重画,界面与交互状态必须**完全一致**。做不到 → 要么前端私存了对局态、要么服务端漏发了字段,二者必居其一,**都要修**。
- 这与「重连即重画」是同一条路径(见 [04 §3.3](./04-网络对接与启动编排.md)):**重连之所以能只靠服务端快照还原,正因为前端从来没有过独占状态。**
### 6.3 允许的本地 UI 态(白名单)
只有**不影响对局裁定、服务端根本不关心**的纯表现态,才可以只存在于前端:
| 允许只在前端 | 不允许(属对局态,必须来自服务端) |
|---|---|
| 选中/待出牌的高亮、拖拽位置 | 这张牌能不能出、出了之后轮到谁 |
| 按钮按下高亮/缩放、点击音效 | 按钮**该不该出现**、**能不能点** |
| 列表滚动位置、面板展开、设置开关 | 阶段、控制权、倒计时基准、分数、结算 |
| 动画进度、特效播放中标记 | 手牌/牌河/副露内容、亮牌信息、托管状态 |
判别:**这个值若被玩家改成任意值,会不会影响对局结果、或让他看到/做到本不该的事?** 会 → 它是对局态,必须服务端权威。
> **与 [04 §5.6](./04-网络对接与启动编排.md#56-受控例外响应交互按钮乐观清除--服务端合法性验证) 受控例外的关系**:本玩家点击响应/掷骰交互按钮后的「乐观隐藏」仍然允许——它是抢先一步做了服务端稍后会确认的事,**不是前端私有状态**:按钮**该不该出现**依旧由服务端下发的 `availableActions` 决定,收包侧必须能独立得出同一结果(AI 托管无点击时也正确)。判据仍成立:丢弃 `this.data` 后按最近快照重画,该按钮的显隐与服务端一致。
### 6.4 组件数据与表现延后
- **组件数据自持 + set-refresh**:组件(及其下每个「零件 UI」)的自有数据集中在 `this.data`,**不散落**各处;每个 UI 都有 `setXxx`(只写数据)/`refreshXxx`(只据数据画界面)成对方法,组件另有总 `refresh()` 据 `this.data` 重建整块界面(见 02)。
- **更新时机——发包只请求、收包才表现(悲观 UI / 输入-渲染解耦)**:用户点击**只发请求包**,**绝不**在点击时改动任何对局状态界面(提示显隐、按钮增删、当前控制权、倒计时启停、落牌、阶段/托管图标);这些表现一律在**收到服务端结果/推送包后**更新。点击回调只做「发包 +(可选)纯本地物理反馈(按下高亮/音效、防连点 `disable`)」。收包处理器**触发源无关**——数据从包字段读、不依赖「点击时暂存的本地变量」,故真人操作与 **AI 托管/他人广播共用同一更新路径、对前端透明**(无点击时也正确更新);把更新挂在「点击」而非「收包」会导致 AI 托管时界面卡死。详见 [04 §5](./04-网络对接与启动编排.md#5-输入渲染解耦发包只请求收包才表现)。**受控例外**:本玩家点击的**响应/掷骰交互按钮**(碰/杠/过/胡按钮、手动掷骰按钮)的**隐藏**允许乐观清除(兼作防连点+即时反馈),前提是**服务端合法性验证 + 收包侧兜底(AI 托管一致)**,见 [04 §5.6](./04-网络对接与启动编排.md#56-受控例外响应交互按钮乐观清除--服务端合法性验证) 与服务端 [04 §8](../../server/development-guide/04-开发规范与红线.md#8-操作请求合法性验证不可信客户端);其余提示/控制权/倒计时/落牌仍严格收包驱动。
- **收包节奏**:**先 `setXxx` 写数据 →(必要时 `refresh` 刷静态界面)→ 再播动画 → 动画回调里只刷新界面**;动画的开始/结束/出错等生命周期回调里**绝不设置核心数据**,动画期间**不改数据**。
- **重画随时可用(断线重连 + 硬刷新都要处理)**:重连/页面重载的本质是**恢复数据 → 调各 UI 组件 `setXxx`/`refreshXxx` 恢复数据与界面状态**;任何时候调 `refresh` 都能据 `this.data` 重建正确界面(断线重连、硬刷新/网页重载、切 app 复用**同一条**重画路径,不为重连单写一套渲染,详见 04 §3.3)。
- **动画是体验层**:开发阶段可**先不做动画**只保证静态界面正确;即使动画缺失/卡住/播错,数据、逻辑与界面仍正确、互不影响。
---
## 7. 成败标志与收发包
- **成败只认 `data.success`**:`if (!data.success)` 判失败;**禁**用 `status`/`code` 判成败、**禁** `status` 兼容兜底。个别嵌套场景按该推送契约取 `success` 所在字段,语义不变。
- **发包走语义化发包封装/`RpcHelper`**(自动注入平台字段),**收包统一分发**(一 rpc 一处理器)。
- 业务逻辑放 controllers/managers,**不**写进受限 `Game_Modify.*`。
---
## 8. shared 同步
- **`shared/` 是子游戏自己的游戏逻辑,与平台无关**:存放本玩法**前后端必须算出完全一致**的纯逻辑(胡牌/听牌/比精/牌型/计分/规则常量等),前端用于即时表现与预校验,服务端做权威裁定。它不是平台代码,平台既不提供也不感知。
- `01_SubGame/codes/shared/` 是服务端 `server/<游戏容器目录>/<游戏>/shared/` 的**同步副本**,前端**只读**(脚本生成)。
- 改共享算法只改服务端权威源,再运行同步脚本覆盖前端副本;**禁止直接编辑前端 `codes/shared/`**。
- 逻辑同源不改变数据权威:前端 `shared/` 算的是表现/预判,最终以服务端为准(数据优先、表现延后,见 §6)。详见服务端 [04 §9 shared 文件同步流程](../../server/development-guide/04-开发规范与红线.md#9-shared-文件同步流程)。
---
## 9. 模块职责边界
- 一个职能只在一个模块实现,其他模块**调用而非重造**:渲染找 `SpriteManager`、动画找 `AnimationManager`(及游戏动画封装)、音频找游戏音频管理、Spine 找 `SpineMgr`(及 Spine 回调分发)、发包找语义化发包封装、收包分发找收包分发器。
- **UI 组件专职自己的界面**:每个界面的数据与渲染**只由其对应 UI 组件实现**,组件对外提供 `setXxx`/`refreshXxx` 与语义化公开方法。别的模块(controllers/managers/其他组件)要改动或刷新某界面,一律**调用该组件的公开接口**,**禁止**在别处重复实现重叠或类似的界面数据/渲染逻辑(否则同一界面出现多份状态源,重连/刷新时必然不一致)。
- **显隐走 `showXxx`/`hideXxx` 接口**:UI 组件及其「零件 UI」的精灵/群组显隐,必须由组件自身暴露的 `showXxx()`/`hideXxx()` 语义接口控制;**禁止**在别处直接用该组件/零件的精灵 ID、群组 ID 去 `SpriteManager.show/hide`(或 `showGroup/hideGroup`)控制其显隐——绕过组件即状态分散,重连/刷新时必然不一致。
- 写代码前先问“这段属于谁的职责”,属于别人就调用它,不在本模块复制一份近似实现。
---
## 10. 测试纪律
- 测试用于验证业务正确性;失败先**裁定根因归属**(业务缺陷 vs 测试脚本缺陷),禁止 skip/软化断言/吞异常掩盖。
- 业务缺陷修业务并单独提交;脚本缺陷修置场并保持硬断言。
- `shared/` 算法变更应在 Node 跑相应单测验证(前后端同源)。
---
## 11. 审查速查表
| 维度 | 红线 |
|------|------|
| 范围 | `vendor`/`00_Surface` 禁改;受限文件不新增接口;`shared` 只读 |
| 语言 | 严格 ES5;新文件插对加载顺序 |
| 框架中立 | 框架无玩法逻辑/专属常量;专属内容归子游戏 |
| 渲染 | 只走 `SpriteManager`;ID 守范围、不编造;查返回值 |
| 常量 | 精灵/群组/图层/图片/声音/Spine/坐标/动画/事件全进常量;UI 代码禁硬编码 id 与裸值 |
| 组件 | 继承 `BaseComponent`;事件 `addEventListener`;`destroy` 清动态精灵/定时器 |
| 数据驱动 | 前端**无对局状态机**:阶段/控制权/可用操作/倒计时/分数只读包内权威字段,禁本地推导与本地推进流程;`this.data` 只是服务端快照的镜像,丢弃后仅凭最近快照重画须完全一致;只有白名单纯表现态可只存在于前端(§6.1–6.3) |
| 发包内容 | 请求包只带「意图」(操作类型+目标标识),**不带结论**(分数/判定结果/阶段指令);服务端按自己的权威数据重算(04 §1「请求包只带意图」) |
| 数据 | 服务端权威(核心运算/裁定在服务端);组件自有数据集中 `this.data`;每个 UI/零件有 set/refresh 对、组件有总 `refresh` |
| 更新时机 | 点击只发请求包,对局状态表现等**收包后**更新(悲观 UI);收包处理器触发源无关、数据从包字段读;AI 托管/他人广播共用同一更新路径。**受控例外**:响应/掷骰交互按钮隐藏可乐观清除(防连点+即时反馈),前提是服务端合法性验证+收包兜底(04 §5.6、服务端 04 §8) |
| 节奏 | 先写数据后表现;动画回调只刷界面、**不写核心数据**;重画可随时据数据还原界面 |
| 成败 | 只认 `data.success`,禁 `status` 兜底 |
| 收发包 | 发走语义化发包封装、收走收包分发器;业务不进 `Game_Modify` |
| 职责 | 一职能一模块,调用不重造 |
| UI 职责 | 每个界面的数据/渲染/显隐只在其 UI 组件实现;别处调组件接口(含 `showXxx`/`hideXxx`),不重叠重造、不直接用其精灵/群组 id 控显隐 |
| 重连 | 断线重连 + 硬刷新都要处理;本质=恢复数据→各组件 set-refresh;复用同一重画路径 |
---
至此 01–05 构成一套完整的前端子游戏开发指导。回到 [README](./README.md) 查看导航与分层模型。