优化开发守则文档

This commit is contained in:
2026-07-05 22:29:20 +08:00
parent 2e59742bc9
commit ae43bd528f
15 changed files with 70 additions and 882 deletions
@@ -9,7 +9,7 @@
## 1. 运行环境
- **部署形态**:前端是**浏览器静态资源**,由 gameabc 引擎(`js/vendor/gameabc.min.js`)驱动,绘制基于精灵(Sprite)/图层(Layer)/群组(Group)的 2D 画面。不走 npm 构建。
- **与服务端**:通过平台网络层收发 JSON 包(`{app, route, rpc, data}`),全程异步;服务端权威,前端只显示与发起操作。
- **与服务端**:通过平台网络层收发 JSON 包(`{app, route, rpc, data}`),全程异步。**服务端权威**——所有核心运算与胜负裁定以服务端为准、数据以服务端为权威;**前端只做界面展示与玩家交互**,本地保存的数据只是「用于渲染的副本」,不做权威计算(前端渲染与交互范式见 02,红线见 05)。
- **ES5 强制**:线上浏览器/平台不保证 ES6+。全部 JS 必须严格 ES5——用 `var`/`function`,对象继承用 `Object.create`,禁 `let`/`const`/箭头函数/模板字符串/`class`/解构/默认参数。
- **少量 Node 仅用于测试/工具**:`shared/` 算法可在 Node 跑单测,`scripts/` 是 Spine 数据构建脚本;线上运行时无 `require`,跨文件一律按**全局名**引用(各文件用 `var X = ...` 暴露全局,`index.html` 顺序加载)。
@@ -169,6 +169,40 @@ this.addEventListener(EventBus.Events.GAME_STARTED, fn);
`BaseComponent.addEventListener` 会包装处理函数(绑定 `this`、捕获异常、销毁后忽略事件),并记录下来在 `destroy()` 时统一 `EventBus.off`。**销毁组件用 `destroy()` 而非只 `hide()`** —— 只有 `destroy()` 才清理监听。
### 组件数据自持 + set-refresh 范式(核心)
**前端只负责界面展示与玩家交互;所有核心运算与胜负裁定以服务端为准、数据以服务端为权威**(见 01/05)。组件本地保存的只是「用于画界面的数据副本」,据此渲染,不做权威计算。为此每个组件遵循统一范式:
1. **数据集中在 `this.data`**:组件(及其下每个「零件 UI」)的自有数据全部收拢到 `this.data.*`,**不散落**在实例其他字段或全局;界面能否重建只取决于 `this.data`。
2. **每个 UI 都有 `setXxx` / `refreshXxx` 成对方法**:组件本身、以及组件内每个「零件 UI」(如玩家信息条、分数、手牌区、按钮组)都提供一对——
- `setXxx(...)`:**只写数据**到 `this.data`,不碰精灵;
- `refreshXxx()`:**只据 `this.data` 画界面**,不改数据。
两者职责单一、互不越界。
3. **组件有一个总 `refresh()`**:顺序调用其下所有零件的 `refreshXxx()`,把整块界面据 `this.data` 完整重画。**任何时候调用 `refresh()` 都能无歧义重建正确界面**(断线重连、切 app 回来、网页刷新都复用它,不另写一套渲染)。
```js
var GameView = Object.create(BaseComponent);
GameView.init = function (config) {
this.data = { players: [], score: 0 /* ...自有数据只放这里 */ };
// ...
};
// 零件 UI:分数。set 只写数据,refresh 只据数据画
GameView.setScore = function (v) { this.data.score = v; };
GameView.refreshScore = function () { SpriteManager.setText(SCORE_ID, this.data.score); };
// 总 refresh:据 this.data 重画所有零件
GameView.refresh = function () {
this.refreshPlayers();
this.refreshScore();
// ...其余零件
};
```
**收包的正确流程**:先 `setXxx` 把数据写进 `this.data`(必要时 `refresh` 刷新静态界面),**再**做动画/表现。动画只是体验层,其「开始/结束/出错」等生命周期回调里**只刷新界面、绝不设置核心数据**。这样即使动画卡住/播错、或开发前期还没做动画,数据、逻辑与界面依然正确、互不影响。
---
## 4. UIManager:注册与场景切换
@@ -295,6 +329,7 @@ list.onClick = function (type, rowIndex, rowData) { /* ... */ };
| 检查 `SpriteManager` 返回值 | 忽略 `false` 返回 |
| 精灵/资源/坐标全进常量文件 | 在业务代码内联裸数字 |
| 组件继承 `BaseComponent`,事件用 `addEventListener` | 自建组件类 / 直接 `EventBus.on` |
| 自有数据集中 `this.data`;每个 UI/零件有 set/refresh 对;总 `refresh` 可据数据重建界面 | 数据散落各处 / `set` 里画界面 / `refresh` 里改数据 / 动画回调写核心数据 |
| 销毁用 `destroy()`,`onDestroy` 清动态精灵与定时器 | 只 `hide()` 就当销毁 |
| 动态列表用 `DynamicSpriteList` 并 `destroy` | 手搓 SpriteCopy 又忘清理 |
@@ -55,12 +55,12 @@ EventBus.once(EventBus.Events.GAME_FINISHED, fn); // 一次性
收到服务端推送后的固定节奏:
```
1) 先更新数据模型(不动 UI)
1) 先更新数据模型(setXxx 写入组件 this.data,不动 UI)
2) 播放动画(纯视觉过渡,不改数据)
3) 动画完成回调里刷新静态界面
3) 动画完成回调里只刷新静态界面(refresh)
```
好处:动画是“锦上添花”,**即使不播动画,静态界面也始终正确**(重画函数据本地数据即可还原,见 04 重连)。**动画期间绝不修改游戏数据。**
好处:动画是“锦上添花”,**即使不播动画,静态界面也始终正确**(据组件 `this.data` 调 `refresh` 即可还原,见 02 组件范式 / 04 重连)。**动画的开始/结束/出错等生命周期回调里只刷新界面、绝不设置核心数据**;动画期间**绝不修改游戏数据**。这样动画卡住/播错、或开发前期没有动画时,数据、逻辑与界面依然正确、互不影响。
### AnimationManager API(框架,通用)
@@ -69,11 +69,13 @@
---
## 6. 数据优先、表现延后
## 6. 数据权威、组件数据与表现延后
- 收到推送的固定节奏:**先改数据模型 → 再播动画 → 动画回调里刷新静态界面**;动画期间**不改数据**。
- **重画函数**据本地数据可随时重建正确界面(如网页刷新);断线重连与切 app 复用同一重画路径,不为重连单写一套渲染。
- 开发阶段可**先不做动画**,只保证静态界面正确;动画作为后续叠加,不影响界面正确性。
- **服务端权威**:所有核心运算与胜负裁定以服务端为准、数据以服务端为权威;前端只做界面展示与玩家交互,本地数据仅为「渲染副本」,不做权威计算。
- **组件数据自持 + set-refresh**:组件(及其下每个「零件 UI」)的自有数据集中在 `this.data`,**不散落**各处;每个 UI 都有 `setXxx`(只写数据)/`refreshXxx`(只据数据画界面)成对方法,组件另有总 `refresh()` 据 `this.data` 重建整块界面(见 02)。
- **收包节奏**:**先 `setXxx` 写数据 →(必要时 `refresh` 刷静态界面)→ 再播动画 → 动画回调里只刷新界面**;动画的开始/结束/出错等生命周期回调里**绝不设置核心数据**,动画期间**不改数据**。
- **重画随时可用**:任何时候调 `refresh` 都能据 `this.data` 重建正确界面(网页刷新/断线重连/切 app 复用同一路径,不为重连单写一套渲染)。
- **动画是体验层**:开发阶段可**先不做动画**只保证静态界面正确;即使动画缺失/卡住/播错,数据、逻辑与界面仍正确、互不影响。
---
@@ -119,7 +121,8 @@
| 渲染 | 只走 `SpriteManager`;ID 守范围、不编造;查返回值 |
| 常量 | 精灵/资源/坐标/动画/音效/事件全集中,禁硬编码裸值 |
| 组件 | 继承 `BaseComponent`;事件 `addEventListener`;`destroy` 清动态精灵/定时器 |
| 节奏 | 先数据后表现;重画可随时还原界面 |
| 数据 | 服务端权威(核心运算/裁定在服务端);组件自有数据集中 `this.data`;每个 UI/零件有 set/refresh 对、组件有总 `refresh` |
| 节奏 | 先写数据后表现;动画回调只刷界面、**不写核心数据**;重画可随时据数据还原界面 |
| 成败 | 只认 `data.success`,禁 `status` 兜底 |
| 收发包 | 发走语义化发包封装、收走收包分发器;业务不进 `Game_Modify` |
| 职责 | 一职能一模块,调用不重造 |
+5 -3
View File
@@ -20,10 +20,10 @@
|----|------|--------------|
| 00 | 本文 README | 这套文档是什么、怎么读、红线速查 |
| 01 | [01-前端架构与运行环境.md](./01-前端架构与运行环境.md) | 双运行时/ES5、平台/框架/子游戏三层、新旧架构并存、目录与加载顺序 |
| 02 | [02-渲染与UI组件体系.md](./02-渲染与UI组件体系.md) | 精灵 ID 体系、SpriteManager 分层、资源常量组织、BaseComponent 组件化、UIManager 场景、动态列表 |
| 02 | [02-渲染与UI组件体系.md](./02-渲染与UI组件体系.md) | 精灵 ID 体系、SpriteManager 分层、资源常量组织、BaseComponent 组件化、组件数据/set-refresh 范式、UIManager 场景、动态列表 |
| 03 | [03-事件·动画·音频·Spine.md](./03-事件·动画·音频·Spine.md) | EventBus、AnimationManager+配置、AudioManager+音效资源、SpineMgr 全链路 |
| 04 | [04-网络对接与启动编排.md](./04-网络对接与启动编排.md) | 发包链路(RpcHelper 注入平台字段)、收包统一分发、新旧架构对接边界、启动编排、处理器/管理器职责 |
| 05 | [05-开发规范与红线.md](./05-开发规范与红线.md) | 可编辑范围、ES5、框架中立、常量集中、组件生命周期、data.success、模块职责、测试 |
| 05 | [05-开发规范与红线.md](./05-开发规范与红线.md) | 可编辑范围、ES5、框架中立、常量集中、组件生命周期、服务端权威/组件数据/表现延后、data.success、模块职责、测试 |
建议第一次**从 01 顺序读到 05**;之后把 02–05 当手册随用随查。
@@ -60,8 +60,10 @@ BaseComponent / UIManager(ui,组件化与场景) ← gameabc-framework(
- **框架游戏中立**:`gameabc-framework/` 内**不得**出现任何具体玩法逻辑或专属常量;玩法专属的事件/资源/配置一律定义在子游戏侧(如玩法事件在子游戏的事件常量文件里追加到 `EventBus.Events`)。
- **精灵只走 SpriteManager**:UI 代码**禁止**直接调 `GameABCUtils` 或引擎原生 API;ID 必须落在规定范围(精灵 1001–3000、群组 ≥201、图层 101–200/弹窗 301–400,另有 501–600、701+ 备用段,框架保留 1–1000 及 3001+ 等交替段)。
- **常量集中、禁硬编码**:精灵 ID/图片资源 ID/坐标尺寸/动画时长/音效 ID 一律定义在对应常量文件,业务代码引用,**不内联裸值**。
- **服务端权威、前端只展示**:核心运算与胜负裁定以服务端为准、数据以服务端为权威;前端只做界面展示与玩家交互,本地数据仅为渲染副本,不做权威计算。
- **组件走 BaseComponent**:UI 组件继承 `BaseComponent`,事件用 `this.addEventListener` 注册(`destroy()` 自动清理,防泄漏);动态精灵/定时器在 `onDestroy` 清理。
- **数据优先、表现延后**:收到推送先改数据模型,再播动画,动画回调里刷新静态界面;动画期间**不改数据**。即使不播动画,重画函数也能据本地数据还原正确界面。
- **组件数据自持 + set-refresh**:组件自有数据集中 `this.data`;每个 UI/零件有 `setXxx`(只写数据)/`refreshXxx`(只据数据画)对,组件有总 `refresh()` 可据数据重建界面。
- **数据优先、表现延后**:收到推送先 `setXxx` 写数据、再播动画;动画的开始/结束/出错回调里**只刷界面、不写核心数据**。即使动画缺失/卡住/出错,数据、逻辑、界面仍正确、互不影响。
- **成败只认 `data.success`**:前端一律 `if (!data.success)` 判成败,**不**用 `status`/`code`,**不**写 `status` 兼容兜底。
- **收发包走统一通道**:发包经统一发送封装(底层 `RpcHelper` 自动注入平台字段),收包统一分发(一 rpc 一处理器);业务逻辑放处理器,**不**写进 `Game_Modify.*`。
- **shared 只读**:`01_SubGame/codes/shared/` 是服务端 `shared/` 的同步副本,**不在前端改**,改服务端权威源后跑同步脚本。
@@ -21,7 +21,7 @@
- `app/route/rpc` 三字段驱动三层路由(见 01)。
- `data` 里**必含平台参数**:`agentid`、`playerid`、`gameid`、`roomcode`、`seat` 等;数值参数收包时要 `parseInt`。
- **一包多信息**:一个响应包应携带本次状态变更的完整信息(出了什么牌、轮到谁、各家剩余、倒计时、比分……),减少往返。
- **一包多信息**:一个响应/推送包应带全「本次状态变更」所需的核心数据(出了什么牌、轮到谁、各家剩余、倒计时、比分……),使前端**仅凭本包 + 已有本地数据**就能把相关界面刷对——既减少往返,也避免"把一个界面状态拆成几个包拼、中途丢一个就停在自相矛盾的中间态"。("发全哪些"的具体要求见 §1.2。)
### 1.1 `route` / `routename` 从哪来、在哪定义、怎么匹配
@@ -44,6 +44,18 @@
> 一句话:**`routename` 在服务端 `cls_mod.new` 第二参定义(自定、应用内唯一),前端发包 `route` 必须与它逐字相同,平台据此把包投到你的模块**;改名要两端同步改。
### 1.2 发包必须自带前端界面所需的全部核心数据
**前端以服务端数据为权威——只做界面展示与交互、不做权威计算**(见 [`client 05`](../../../client/docs/development-guide/05-开发规范与红线.md))。推论落到服务端:**每个下发包都必须携带前端渲染/刷新该界面所需的全部核心数据**,让前端「据包写入数据 → 直接刷新出界面」,而**不需要前端自行推算、补全或兜底**。
- **界面要用的字段都要发全**:凡界面要显示、或前端 set/refresh 要用到的核心字段(出了什么牌、轮到谁、各家剩余张数、手牌/副露、分数/比分、倒计时锚点、庄家/座位、各类状态标志……)都要放进 `data`,不能让前端"猜"或本地推算权威结果。
- **漏发是服务端的缺陷,不许前端补**:前端遵循数据权威原则——权威字段缺失应显式报错/留空、不用 `|| 0`/`|| []` 兜造(见 client 05)。所以服务端漏发 = 前端界面缺数据,**修在服务端发包处,不在前端补洞**。
- **落实「一包多信息」(见 §1)**:本包要让前端**仅凭本包 + 已有本地数据**把相关界面刷对,别把一个界面状态拆成几个包拼(中途丢一个就停在自相矛盾的中间态)。
- **差异化但要发全**:每个座位只发它**该看到**的核心数据(手牌只发本人,见 §3),但"该看到的"必须发全、发准。
- **重连/中途加入发完整快照**:`get_deskinfo` 必须给出该座恢复整个界面所需的**全量**核心数据,让前端 `Reconnect` 据快照一次重画(见 §6.4、02)。
> 一句话:**服务端权威不仅是"服务端算",也是"服务端把界面要用的核心数据发全"——前端只据包渲染,不推算、不兜底。**
---
## 2. 收包处理的固定步骤(在 handler 里)
@@ -254,6 +266,7 @@ if (!data.success) { /* 失败处理 */ return; }
## 8. 小结
- 包结构恒为 `{app, route, rpc, data}`;收包先 `check_player`,失败静默 `return`。
- **下发包必须发全前端界面所需的核心数据**(§1.2):前端以服务端为权威、不自算,漏发修在服务端发包处、前端不补洞。
- 三种发包方式按"谁该看到什么"选;差异化广播逐座位定制,敏感信息只发本人。
- **主动推送是唯一可靠下发通道**,`return` 不算。
- **成败只认 `data.success`**,推送必自带 `success`,禁止 `status`/`code` 判成败与兼容兜底。
@@ -60,6 +60,7 @@ function bar() { var GSM = require('./dataStructures/GameStateManager.js'); }
- **禁止猜测兜底**:不用默认值掩盖缺失数据。**关键权威字段缺失要显式报错或返回 `null`**,由上层决定是否终止,**禁止** `|| 0`、`|| []`、`|| ''`、双源回退等掩盖。
- **下游不修补**:下游不得为"让流程跑起来"而补写/重算上游本该提供的数据;发现下游在修补,应把责任前移到权威来源。
- **边界**:只有展示层、纯 UI 兼容、非关键日志字段,才可在明确边界内用安全默认值。
- **下发面要发全**:因前端以服务端为权威、不自算权威结果,服务端**每个下发包必须携带前端界面所需的全部核心数据**(界面要显示、或前端 set-refresh 要用的字段都发全);漏发 = 前端缺数据,**修在服务端发包处、前端不补洞**(详见 [03 §1.2](./03-数据收发与通信协议.md))。
> 审查信号:看到 `|| []`、`|| 0`、`|| ''`、三元默认、双源字段,先判断它是不是**权威字段**(影响发牌/庄家/手牌/规则/结算/重连/状态流转)。是 → 改成权威读取;只是展示/日志 → 才可保留默认。
@@ -167,7 +168,7 @@ function bar() { var GSM = require('./dataStructures/GameStateManager.js'); }
| 范围 | 只改子游戏目录 `<容器目录>/<游戏>/` 与允许的前端范围 |
| 语言 | 纯 ES5;`require` 仅在顶部守卫块 |
| 成败 | 只认 `data.success`,推送必自带,禁 `status` 兜底 |
| 数据 | 权威唯一、缺失报错/返回 null、禁兜底、下游不修补 |
| 数据 | 权威唯一、缺失报错/返回 null、禁兜底、下游不修补;下发包发全前端界面所需核心数据 |
| 职责 | 一职能一模块,调用不重造 |
| 隔离 | 状态挂 `o_desk.data.*`,禁全局;`房间+seat` 作 key;定时器随房清理 |
| 自动操作 | 复用真人链路,对前端透明 |
+2 -1
View File
@@ -84,7 +84,7 @@ o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下
- **模块职责边界**:一个职能只在一个模块实现,其他模块**调用而非重造**。
- **房间隔离**:一切随对局变化的状态都挂 `o_room.o_desk.data.*`;**禁止模块级单例/全局变量存对局态**;缓存/定时器/决策表不得只用 `seat` 作 key,必须 `房间 + seat`;结束/解散/开新局时按房间清理全部定时器与残留状态。
- **服务端自动操作复用真人链路**:服务端代替玩家做的操作(如 AI 托管)必须复用「真人操作」的同一套处理入口与广播链路,对前端透明,不为它单开一套下发分支。
- **服务器权威**:房卡、胜负、积分、状态等关键数据与操作一律以服务器为准,前端数据仅供显示。
- **服务器权威 + 发全下发面**:房卡、胜负、积分、状态等关键数据与操作一律以服务器为准、前端只作显示不自算;因此**服务端每个下发包必须携带前端界面所需的全部核心数据**,漏发修在服务端发包处、前端不补(详见 03 §1.2 / 04 §4)。
- **测试纪律**:测试失败先用证据裁定「业务缺陷 vs 测试脚本缺陷」,禁止 skip/软化断言/吞异常掩盖;**正式代码不得为兼容测试而加逻辑**。
---
@@ -94,5 +94,6 @@ o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下
- 平台级、面向「所有子游戏」的总纲在 `docs/important/server/`(友乐框架收发包规范、子游戏开发要求)。
- 本套文档是其**落地版**:结合本仓库真实代码,给出可直接照做的接入步骤与红线,并对其中已被本项目实践修正的部分(如成败标志由 `status` 收敛为 `success`)以本套为准。
- 各子游戏内部的架构细节(模块划分、算法)仍以各自 `<游戏容器目录>/<游戏>/docs/` 为准。
- **平台无关的通用工程与架构规范**(分层、可扩展模式、配置化、数据权威、反模式与审查清单)见 [`docs/engineering/`](../../../docs/engineering/):本套讲"平台怎么接、红线是什么",`engineering/` 讲"该怎么设计、怎么长久演进",互补阅读。
</content>
</invoke>