从“能跑一整局的 Demo”到“可联调的成品”:一次五子棋联机高保真重构复盘
一开始,我以为这个项目已经差不多做完了。
因为从功能层面看,它并不差:能创建房间、能加入房间、能通过 Socket 联机、能完整下一整局棋,判赢也能跑,演示的时候看上去并没有什么明显问题。放在很多场景里,这样的结果已经足够被称作“一个做出来的 Demo”。
但这次在按原图重新做高保真重构,并把前后端联调真正跑顺之后,我才非常明显地意识到一件事:
“能跑的 Demo”和“可交付的成品”,中间隔着的不是几个功能点,而是一整套边界控制、状态治理和工程收束能力。
这篇文章,不是想讲五子棋本身怎么写,而是想复盘一个更真实的问题:
为什么一个“看起来已经做完”的前端 Demo,在真正按需求落地的时候,会突然暴露出这么大的差距?
一、项目表面上很小,实际并不轻
如果只看需求描述,这个项目非常容易被低估。
目标看起来很简单:
- 按设计稿高保真还原 3 个页面
- 支持房间创建与加入
- 支持局域网联机
- 支持悔棋
- 支持最后一步红点标记
- 支持移动端全屏适配
很多人第一反应都会是:
“这不就是一个小游戏前端吗?AI IDE 补一补,几小时就差不多了。”
我一开始其实也有类似判断。毕竟自己之前已经做过一个版本,基础逻辑和联机流程都通了,甚至已经可以完整下一整局棋。当一个项目已经满足“能玩”的时候,人很容易高估它离“做完”还有多近。
但真正开始重构后,问题就一点点露出来了。
这个项目真正的难点,不在于某一项功能本身,而在于它同时叠加了几种很容易互相打架的约束:
- 视觉约束:不能自由设计,必须贴原图
- 状态约束:页面切换不能靠感觉,必须由房间状态驱动
- 通信约束:Socket 事件、payload、状态枚举必须与服务端一致
- 渲染约束:棋盘不是普通 DOM,而是 Canvas,需要处理尺寸、自适应、点击换算和高 DPI
- 联调约束:不仅要“看起来对”,还要在双端操作、悔棋、重开、返回大厅时都正常
这些约束叠在一起以后,它就不再是一个“很快能糊出来的小页面”,而更像一个中小型实时交互前端模块。
二、项目真正失控的起点,不是代码,而是“AI 的过度理解”
这次复盘里最值得说的,其实不是某段代码,而是 AI IDE 的角色。
一开始之所以会跑偏,并不是因为 AI 不会写,而是因为它太会“补全”了。
问题在于,这种补全很多时候不是你要的。
比如你给它的真实任务是:
- 按原图做
- 不要改布局
- 不要改气质
- 只在现有结构里补功能
但如果约束不够严格,AI 往往会自动理解成:
- 我可以顺手优化布局
- 我可以补完整个大厅页
- 我可以加入更常见的产品模块
- 我可以让页面“更合理”“更完整”“更现代”
于是很快就会出现这些问题:
- 原图里没有的 topbar、快捷入口、聊天区、状态栏被补出来了
- waiting 页从“安静的对弈室”等待页,变成了“功能完整的房间大厅”
- game 页的棋盘被缩小,信息栏反而占了主视觉
- 视觉气质从原图的深蓝金色克制风,慢慢偏成另一套更“标准”的游戏 UI
这一步给我的冲击其实很大,因为它让我非常明确地意识到:
AI 最大的优点是补全,最大的风险也是补全。
在工程场景里,很多时候你最需要的不是它“帮你想更多”,而是它不要多想。
三、这次真正起作用的,不是继续生成代码,而是先“锁边界”
项目后来能被拉回来,核心不是因为我们又换了一版代码,而是因为方法变了。
在前期几次跑偏之后,我开始意识到,继续让 AI 一边理解需求一边往下写,只会不断引入新的偏差。真正有效的方式,是先让它停下来,先把边界说死。
后来整个流程逐步稳定下来,靠的是这几步:
1. 先提取视觉锚点
先明确每个页面里绝对不能动的东西:
- lobby 的大标题、左侧装饰棋盘、右侧主卡片
- waiting 的三栏构图
- game 的左右玩家卡 + 中央棋盘 + 底部操作栏
2. 明确禁止新增内容
把原图里没有的内容直接列成黑名单,比如:
- 顶部复杂导航
- 快捷入口
- 聊天区
- 在线人数
- 资产模块
- 多余统计面板
3. 先压最小 HTML 骨架
不是一开始就让 AI 做完整产品,而是先让它只输出:
- 最小 screen 结构
- 最小 DOM 骨架
- 最少必要容器
4. 再逐步补 CSS、补 JS
顺序改成:
- HTML 骨架
- 基础 CSS
- 页面 CSS
- 状态层
- UI 层
- 棋盘层
- Socket 层
- App 协调层
这个过程本质上是在做一件事:
把 AI 从“设计师”切换成“施工队”。
一旦边界先被锁住,后面的代码补全才真正开始变得有价值。
四、我以前那个版本为什么“能跑”,但还是不像成品
这次让我最有感触的一点,是重新理解了“Demo 做完”和“项目做成”之间的差别。
我之前那个版本,客观上已经不是玩具了。
它可以完整对弈,逻辑链路也基本闭环,作为一个演示版本其实已经能说明很多问题。
但它的问题在于:
1. 它解决的是“有没有”
比如:
- 有没有房间系统
- 有没有联机
- 有没有落子
- 有没有判赢
- 有没有页面切换
这些答案都是“有”。
2. 但它没有真正解决“为什么稳”
比如:
- 页面切换是不是状态驱动的?
- 是否和原图足够一致?
- UI 和 Socket 是否耦合过重?
- 返回大厅会不会残留旧状态?
- 重开/悔棋这些边缘流程是否可控?
- 代码结构是否经得住继续加需求?
这些问题,在 Demo 阶段经常会被忽略,因为它们不会立刻阻止功能演示成功。
但到了真正想把东西做“像样”的阶段,这些问题会一起冒出来。
也正是在这个过程中,我才真正理解到一句话:
Demo 证明你会把功能拼起来,成品才证明你能把系统收住。
五、这次最重要的工程收益:不是页面,而是分层
从工程角度看,这次最值钱的结果,其实不是某个页面做得多像,而是最终形成了一个比较清晰的前端分层:
state.js:收敛全局状态树ui.js:只负责 DOM 更新与弹窗/提示board.js:只负责 Canvas 棋盘绘制和坐标换算socket.js:只负责通信和状态同步app.js:只负责初始化、事件绑定和模块协调
这个拆分听起来普通,但对这种项目来说非常关键。
因为五子棋联机这种东西,非常容易长成下面这种“能跑但不敢动”的结构:
- Socket 收包时直接改 DOM
- 点击棋盘时顺手判赢顺手弹 toast
- 页面切换和房间状态混在一起
- 棋盘绘制和 UI 状态写在同一个文件
- 返回大厅时靠刷新页面兜底
这样写不是完全不能跑,但一旦你继续加:
- 悔棋
- 最后一步红点
- 再来一局
- 返回大厅
- 移动端适配
- 断线重连
整个结构会迅速变脆。
而这次至少做到了:每一层各管一件事。
这意味着它已经不再只是“一个能演示的原型”,而是一个可以继续长功能的基础盘。
六、联调前最值钱的一步:读服务端代码,做协议对表
如果说这次前端部分的分层是工程上的收获,那联调前的协议对表就是项目成功的关键节点之一。
因为前端在没有明确协议之前,非常容易陷入一种假象:
“我大概知道服务端会返回什么。”
但“大概”这个词,在 Socket 项目里是最危险的。
后来真正去读 server.js,把这些内容一一对齐之后,很多问题一下就清楚了:
- 客户端 emit 的事件名是否真实存在
- 服务端返回的 on 事件名是否匹配
- payload 字段结构是否一致
- 状态枚举值是不是同一套
- 有没有前端擅自虚构的协议方法
比如这次就发现了两个典型问题:
replyRestart是前端脑补出来的,服务端实际没有resignGame()也是前端默认以为会有,但后端并未实现
如果这两处不提前对表,到联调阶段就会出现一种非常浪费时间的情况:
- 页面看起来没问题
- 按钮逻辑也写了
- 但就是点了没反应
这种问题表面像前端 bug,实质上是协议不一致。
所以这一步给我最大的提醒是:
实时项目里,前端写得再漂亮,如果协议是猜的,本质上都不算完成。
七、从这次经历里,我学到的不是“怎么写五子棋”,而是“怎么收束一个项目”
如果只从技术点来说,这个项目涉及的东西都不算特别新:
- HTML / CSS
- JavaScript
- Canvas
- Socket.io
- 状态管理
- 页面切换
单独拿出来,每一样都不至于难到离谱。
但真正难的,是把它们放在一起以后,依然能保持:
- 设计不跑偏
- 逻辑不打架
- 状态不乱流
- 通信不猜测
- 结构不塌掉
也正因为这样,这次最大的成长其实不是“学会了某个 API”,而是第一次更清楚地理解了一个前端项目在变成成品之前,究竟需要经历什么:
不是只会写
还要会:
- 锁边界
- 控复杂度
- 做分层
- 对协议
- 查风险
- 修收口
这些东西平时在做原型时感受不深,但一旦你真的想把东西做成成品,它们就会变成决定成败的关键。
八、结语:AI 能帮你提速,但工程收束必须靠人
如果让我用一句话总结这次复盘,我会这么说:
AI 可以帮你快速把房子搭起来,但房梁怎么放、承重怎么做、线路怎么走、哪里不能改,最终还是要靠人来做边界控制。
这次项目最开始差点被做成一个“功能看着很多,但结构和视觉都失控”的缝合怪。
但也正因为经历了这轮纠偏,我反而更清楚地看到了:
- AI 适合补全
- 人负责判断
- Demo 解决的是可行性
- 工程解决的是成立性
以前我会觉得“能下一整局就算做完了”。
现在更愿意承认:那只是做出了一个功能闭环的 Demo。
而这次,才算是真正逼近了一个可联调、可维护、可继续扩展的成品前端。
如果以后再遇到这种“看起来不大、做起来却像真项目”的需求,我大概不会再低估它了。
因为我已经很清楚地知道:
真正难的,从来不是把功能写出来,而是把所有边界一起收住。