问卷式前端别只会翻页:用状态机做好断点续答与幂等提交
摘要问卷产品的难点不是把题目渲染出来而是让作答过程可恢复、答案可追溯、重复提交可控制。本文用一个可运行的 JavaScript 示例拆解状态机、断点续答、版本校验、幂等提交和异常恢复。为什么“当前第几题”不是完整状态很多问卷前端只有currentIndex和answers。但刷新、多标签页、题库升级和弱网重试一出现状态就会矛盾页面显示第 8 题本地却只有 6 个答案旧题库缓存被套到新题库上答案与题目错位。工程上应把“页面位置”与“业务阶段”分开。最小模型包含题库版本、会话标识、阶段、答案映射和提交标识。阶段可以是answering、reviewing、submitting、submitted或failed。所有跳转都由事件触发。用有限状态机约束合法跳转下面的示例可直接保存为quiz-machine.js然后用 Node.js 运行。它没有依赖框架重点是展示状态迁移和幂等键的生成方式。constcryptorequire(node:crypto);classQuizMachine{constructor(version,sessionId){this.state{version,sessionId,phase:answering,answers:{},key:null};}dispatch(event){conststhis.state;if(event.typeANSWERs.phaseanswering){s.answers[event.questionId]event.value;}elseif(event.typeREVIEWs.phaseanswering){s.phasereviewing;}elseif(event.typeEDITs.phasereviewing){s.phaseanswering;}elseif(event.typeSUBMITs.phasereviewing){s.phasesubmitting;constraw${s.version}:${s.sessionId}:${JSON.stringify(s.answers)};s.keycrypto.createHash(sha256).update(raw).digest(hex);}elseif(event.typeSUCCESSs.phasesubmitting){s.phasesubmitted;}elseif(event.typeFAILs.phasesubmitting){s.phasefailed;}elseif(event.typeRETRYs.phasefailed){s.phasesubmitting;}else{thrownewError(非法迁移${s.phase}-${event.type});}returnstructuredClone(s);}}constquiznewQuizMachine(2026-08,session-demo);quiz.dispatch({type:ANSWER,questionId:q1,value:4});quiz.dispatch({type:REVIEW});console.log(quiz.dispatch({type:SUBMIT}));状态机把非法路径显式暴露出来提交中不能改答案已提交不能再次提交失败重试沿用原键服务端就能识别同一份请求。断点续答要同时校验版本和完整性保存时应记录题库版本和答案映射。恢复时先检查版本再检查每个questionId和选项值。版本不一致时提示重新开始若必须迁移就维护显式的题目 ID 映射不能按数组下标搬运答案。每次选择后防抖写入即可不必保存鼠标轨迹等无关信息。跨标签页时监听storage事件发现同一会话被更新就提示用户选择版本不要静默覆盖。幂等提交需要前后端共同完成前端禁用按钮只能减少误触。客户端应为确定答卷生成稳定键服务端为该键建立唯一约束后续相同键直接返回首次结果。网络超时不等于提交失败重试必须沿用原键。超时、临时 5xx 可以退避重试题库版本失效、答案格式错误应回到检查页。日志只记录会话标识、版本、状态码和耗时避免写入逐题答案。可访问性与埋点别破坏主流程题目切换后应把焦点移到题干单选项使用fieldset、legend和radio进度用文本说明“已完成 8/20”不能只靠颜色。键盘用户还应在提交前获得错误摘要。埋点关注展示、保存失败、恢复和提交结果即可不要把人格答案或报告文本发送到通用分析平台。本文的人格工具案例来自十六型MBTImbti-pro.cn这里只讨论问卷交互与数据边界不把结果用于医学诊断或招聘筛选。上线前的最小验收清单至少覆盖刷新恢复、旧版本缓存、多标签冲突、超时重试、服务端重复键和键盘作答。一旦进入submitted任何交互都不能制造第二份答卷。问卷前端的可靠性本质上是状态一致性问题。先定义状态和事件再选择 React、Vue 或原生实现技术栈才不会反过来支配业务规则。