从一次真实的体验割裂说起
宝可猜谜挑战是一款有九个每日玩法的微信答题小程序。最近有用户反馈:重玩往期随机题时,猜了四次、范围已经收窄,中途接个电话退出去,再进来变成另一道题,前面所有努力白费。这种割裂感很挫败。
我接到的需求很简单:退出前把"当前这局"存下来,再次进入时自动恢复。但真要把它做对,得先把"状态从哪来、存什么、恢复时哪些派生字段必须一起刷"想清楚。这也是这篇文章想讲的重点——一个看起来不大的功能,背后是状态管理的工程判断。
动手前先确认三条约束
我没有一上来就写代码,而是先确认了三件"仍然成立"的约束:
第一,重玩局不落库。utils/reportFlush.js 在 mode !== "common" 时直接 return,utils/quizClock.js#start 对 replay 不计时。这意味着续玩是纯前端改动,后端零改动,不影响战绩、排行榜、首页"今日 x/9"。
第二,题目由 replayDate 唯一确定。八个本地玩法走 pickByDay(replayDate),数独后端 seed = daysBetween(EPOCH, replayDate) 生成确定性棋盘,道具后端 get(replayDate)。结论很关键:我不需要存题面,只要存 replayDate 就能原样重放同一局。
第三,只有道具是服务端判分(/api/guess 回 clues)。因此道具的存档必须连同 attempts(含 clues)与 spriteUrl 一起保存,其余玩法只存本地进度。
这三条让改动范围收敛得很干净:不需要新接口,不需要改数据库,就是一个前端状态持久化加恢复的工程活。
技术方案
新增 utils/replayKeep.js:每玩法一个 key rpk:<quiz>,覆盖式只保留最近一局。TTL 用写入时间戳判定,而不是按日期分桶——这里有个细节,23:50 玩到 00:10 的局若按日期分桶会被当成跨天丢弃,进度就丢了,所以必须用时间戳。
新增 components/resume-bar:顶部提示条"已恢复上一局(已猜 3/6)· 换一题 ›",出第一手即收起。配色用中性灰米,不复用好友邀请条的橙色语义,避免用户误读成社交动作。
接入九个页面,每页四处:loadReplay/fetch 开头调用 resumeFromKeep();猜完 save;收局/放弃/换一题 clear。恢复逻辑必须插在 replayGuard.shouldRetry 之前,否则这局会被当成撞题重抽。恢复时要一起刷新派生字段:dex 的 textMask、ability 的 effectMask、silhouette 的 zoom/colorSlots,以及 _targetId/_attempts。五个使用 renderBoard(prog, m, replayDate) 的页面直接复用渲染;数独把 fetch 内渲染块抽成 applyBoard(r, m) 供两处共用。
真机第一次跑就暴露的三个坑
我以为逻辑想清楚了就稳了,结果真机一运行,三个缺陷全蹦出来:
导航栏标题污染。dex/silhouette/ability 三页重玩态标题一直显示
DAY #N,因为navTitle.set只在每日路径调用,重玩态漏了。补set(key, "replay")对齐其余五页。上报泄漏。itemQuiz 的"放弃本局"在 replay 下仍会发
/api/report,与"重玩不落库"直接冲突,会把这局当成今天的成绩落库。改成 replay 下只清档、直接 return。运行时引用错误(最隐蔽)。
statsQuiz/moveQuiz/wordleQuiz三页根本没有QUIZ常量——它们用PROG_KEY加各自的loadProgress/saveProgress,不走dailyCache。新代码引用QUIZ直接抛ReferenceError: QUIZ is not defined;lint 静态检查抓不到,只有真机运行才报。三页补const QUIZ = "stats|move|wordle"。
方法论沉淀
这个功能本身不大,但它再次验证了我维护多页面小程序的一个习惯:跨页面复用能力前,先用 grep "^const QUIZ" 把每个页面的实际常量和依赖盘点一遍,比"其它页都有"的假设可靠得多。
续玩类功能的核心,在于厘清三件事:哪些状态必须持久化、哪些可以由一个锚点(这里是 replayDate)重放、恢复时哪些派生字段必须一起刷新。把这三件事想清楚,纯前端就能解决一个看似要动后端的问题。
如果你也在维护一个多玩法的微信小程序,欢迎在评论区聊聊你们怎么处理续玩与存档。