写后端七年,用户录入数据丢失是挨骂最多的一类事故。填了半小时的表单,提交时网络抖了一下,页面一刷新全没了------用户不会管是网络问题还是浏览器问题,他只知道你把他的半小时弄丢了。去年做设备巡检系统时,我们把草稿自动保存当成一级需求来做,这篇记录完整实现。结论先行:自动保存的核心策略是本地优先------输入即存本地,网络恢复再同步,把保存这个动作从网络依赖里彻底解放出来。
一、丢数据事故的解剖
1.1 三次投诉的共同点
项目早期连续收到三次数据丢失投诉,复盘发现共同点:都是移动端填表,都发生在车间网络边缘区,都是填了二十分钟以上才提交,提交失败后页面回退,内容全没。传统网页表单的保存模型是提交式------数据一直活在前端内存里,只有点提交那一刻才落库,中间任何一环断掉,一切归零。填得越久,损失越大,而这个模型对车间场景几乎必然失效:车间的网络覆盖天生有死角。
1.2 为什么浏览器自动填充救不了
有人建议靠浏览器自动填充,行不通:自动填充只认登录态和固定字段名,我们的表单是动态的,字段随设备类型变化;自动填充触发时机不可控,用户感知不到存没存,反而不敢信;多设备场景直接废,手机上填一半换电脑,浏览器填充无能为力。结论明确:自动保存必须由应用自己实现,语义化的、可感知的、跨端的,浏览器的能力只能算彩蛋。
1.3 需求定级
把需求拆成三条硬指标:任何输入在三秒内得到持久化保障;断网状态下可继续完整填写,联网后自动补传;多设备间草稿可接力。第三条是从实际场景倒推出来的------巡检员在车间用手机填到一半,回到办公室想用电脑继续,接力必须在产品里是顺手的动作而不是黑科技。
二、本地优先的实现
2.1 本地存储的选型与结构
存储基座用浏览器本地数据库,每张草稿一条记录,结构设计:
javascript
const draftKey = `draft:${formId}:${bizId}`;
async function saveDraft(formId, bizId, data) {
const record = {
version: Date.now(), // 乐观锁版本
status: 'local', // local | synced
payload: data,
savedAt: new Date().toISOString()
};
await localDB.put('drafts', record, draftKey);
scheduleSync(formId, bizId);
return record.version;
}
要点两个。版本号用时间戳:并发编辑时后写的覆盖先写的,简单但够用------单人单草稿场景下这是最经济的选择;状态字段区分本地与已同步:同步成功标记 synced,同步失败保持 local,恢复网络后同步器只捞 local 状态的记录,扫描代价恒定。
2.2 三层节流策略
输入即存不代表每个按键都写库,节流分三层:按键层防抖三百毫秒,停止输入才算一次变更;变更层合并,五百毫秒窗口内的多次输入合成一次存储;同步层批量,攒五条草稿或间隔十五秒再发同步请求。三层下来,存储写入频率被压到人类输入节奏的几十分之一,手机端的电量损耗几乎无感,而任何时刻断网,丢失的输入不超过一秒------三秒保障的承诺就是这么兑现的。
2.3 同步器与冲突处理
后台同步器常驻,网络恢复事件触发立即扫描:
pseudo
function syncLoop():
drafts = localDB.query(status == 'local') limit 20
for d in drafts:
try:
server.save(d.key, d.payload, d.version)
d.status = 'synced'; localDB.put(d)
except ConflictError: # 服务端版本更新
d.status = 'conflict'
promptUser(local=d, server=serverVersion(d))
冲突处理的原则是服务器优先但绝不静默:服务端版本更新说明另一台设备已提交,本地变成冲突态,弹出对比界面让用户选。自动保存最忌讳的是静默覆盖------用户以为存上了,结果被另一个设备的旧版本盖掉,这种事故比丢数据更伤信任。让用户在冲突时做一次明确的选择,是体验和安全的平衡点。
2.4 草稿接力
多设备接力靠同步器自然达成:草稿同步到服务端后,另一台设备打开表单时先查服务端草稿,有就拉下来续填。接力的关键是草稿和正式数据同源管理------草稿存在服务端的独立草稿表里,按用户和业务键索引,不进正式业务表。曾考虑过草稿直接写业务表加状态字段,否掉:业务表被半成品数据污染后,统计和权限逻辑全要打补丁,草稿的归草稿,成品的归成品,边界干净。
三、细节里的体验
3.1 保存状态要可见
自动保存必须让用户看见:表单右上角一个小状态标,三种形态------灰点正在保存、绿勾已保存、黄色断网本地保存中。状态可见是为了建立信任:用户看不见保存动作,就不敢相信真的存了,行为会退回到反复手动提交。上线后有个巡检员专门反馈:看到绿勾才敢锁屏去干活,这个小标比任何说明文档都管用。信任的建立靠反馈,不是靠承诺。
3.2 草稿的生命周期
草稿不能无限期活着:七天未更新的草稿自动提示处理,三十天未处理的归档清除。生命周期的意义是防止草稿区变成垃圾场------用户草稿列表里堆满半年前的残骸,找东西比填表还累。清理前发一次提醒,给用户最后的取舍机会。另外提交成功后草稿即刻删除,这个动作要在提交事务里原子完成,否则会出现提交成功草稿还躺着的灵异现象,用户分不清哪个是准的。
3.3 存储占用的控制
本地存储有配额,超了会被浏览器清场。两条控制:单条草稿超过两兆自动瘦身------大附件不入草稿,只存引用标记,同步时再传原件;本地已同步草稿只保留最近两百条,更早的服务端有,本地清掉。两条规则实施后,本地占用稳定在几十兆以内,配额告警再也没有触发过。
四、上线后的数据
自动保存上线一年,数据丢失投诉清零。草稿功能的实际使用率远超预期:巡检员平均每天产生三点五条草稿,其中一成二最终没有提交------这批草稿以前就是丢失数据,现在成了管理信号:填了没交的表单集中在哪类设备、哪个时段,分析后发现是某类点检表字段设计过繁,优化表单后草稿废弃率降了一半。丢数据的问题做成了数据资产,这是当初没敢想的意外收获。
五、总结
表单自动保存的实现框架:本地优先,输入即存本地库,网络从保存链路里出局;三层节流,防抖、合并、批量,把写入频率压到无感;同步冲突显式处理,服务器优先但不静默覆盖,用户的信任比自动化的优雅更重要;保存状态可见,灰点绿勾黄标,信任是设计出来的不是承诺出来的。断网不丢数据听起来是体验细节,本质是移动场景下的架构选择------把数据的最后一道防线放在离用户输入最近的地方。
常见问题
问:本地存储会不会被用户清缓存清掉?
答:会,这是本地方案的固有边界,要靠服务端同步兜底:同步间隔足够短,丢失窗口就足够小。我们的常态是三十秒内必同步一次,用户清缓存的极端场景,损失不超过三十秒的输入。另外清缓存是显式动作,把它和断网场景区分开:断网有本地保障,清缓存前用户理应知道后果。双保险再加一条:重要表单提交前的最终确认页保留快照,给最后一层挽回余地。
问:多人协作填同一张表怎么处理草稿?
答:先从产品上拆:协作场景拆成多人各填各的子表,再汇总合并,草稿按人按子表隔离,天然无冲突。确实要共写一个字段的,草稿退化为提示工具:后进者打开时提示当前内容已被他人更新,展示最新值,本地草稿只做参照不参与保存。共写字段在移动场景本身就是反模式,能用结构拆解就别用锁去扛。
问:草稿同步到服务端,占不占正式数据的存储和备份?
答:占,但要隔离。独立草稿表独立备份策略:草稿备份频率低于正式数据,保留期短于正式数据,成本自然可控。量级参考:我们巡检系统一年草稿量约等于正式数据的一成五,隔离后备份成本几乎可以忽略。切忌草稿进正式表加状态位,统计口径、权限判断、数据清理全会被半成品数据拖累。
问:表单结构改版后,旧草稿的字段对不上怎么办?
答:草稿带表单版本号,加载时按同版本渲染;版本不一致的草稿,做字段级迁移映射:新字段有对应旧字段的自动带入,没有的置空并提示。映射规则随表单改版一起维护,是改版流程的必填动作。实在无法映射的老草稿,保留原样展示允许手工誊抄------宁可贵一点,也不静默丢弃用户填过的内容,这是自动保存体系的底线伦理。