返回探索
指南VibeFix 编辑部更新于 2026年10月17日

用户写了 2000 字然后丢了:vibe 项目的草稿自动保存实战

用户写的 2000 字,要扛住浏览器崩溃、断网和误关标签页。三层防线——输入防抖、localStorage 兜底、带版本号的服务端持久化,外加版本号乐观锁的冲突 UX(保留两份让用户选)、离线重放与草稿生命周期。

实拍:伏案写作的人,笔记本旁放着电脑

本文只讲一件事:单人场景的草稿自动保存——用户写了 2000 字,浏览器崩溃/断网/误关标签页之后,字还在。先划清边界:

丢稿是 vibe 项目里最伤用户信任的事故之一:它不像 500 错误那样"修好了就行",用户丢的那 2000 字是找不回来的。而 AI 生成的编辑器代码,十有八九只有个 onChange 直接调保存接口——没有防抖、没有本地兜底、没有冲突处理。本文给你三层防线,一套能直接抄的方案。

三层防线:丢稿事故都是某一层缺了

层防什么实现恢复体验
1. 输入防抖(内存)频繁写入打爆接口停止输入 800ms 后触发保存即时,无感
2. localStorage 兜底浏览器崩溃、标签页误关每次防抖触发时同步写本地重开页面提示"恢复草稿"
3. 服务端持久化换设备、清缓存防抖后异步调接口,带版本号任何设备登录都接着写

三层缺一不可,缺哪层、丢哪种稿:

  • 缺第 1 层:用户每敲一个字就调一次保存接口,服务端被打爆,或者你为了省钱把保存间隔设成 30 秒——用户在第 29 秒关页面,29 秒的字全丢。
  • 缺第 2 层:浏览器崩溃是最常见的丢稿场景。服务端保存再快,也救不了"还没发出去的那 800ms"。
  • 缺第 3 层:用户手机上写一半,回家想用电脑接着写——没服务端,草稿只活在手机浏览器里。
// 三层防线的最小实现(伪代码,框架无关)
let timer = null;
let version = 0; // 服务端版本号,见第三节

function onInput(content) {
  // 第 1 层:防抖
  clearTimeout(timer);
  // 第 2 层:每次输入都同步写本地(便宜,可靠)
  localStorage.setItem('draft:' + docId, JSON.stringify({ content, ts: Date.now() }));
  setSaveStatus('editing'); // "编辑中…"
  timer = setTimeout(() => saveToServer(content), 800);
}

async function saveToServer(content) {
  setSaveStatus('saving'); // "正在保存…"
  // 第 3 层:带版本号的服务端保存
  const res = await api.put(`/docs/${docId}/draft`, { content, version });
  version = res.version;
  localStorage.removeItem('draft:' + docId); // 服务端落盘后清本地
  setSaveStatus('saved'); // "已保存 12:03"
}

防抖参数:300ms 还是 2s?看内容类型

防抖时间不是拍脑袋的,给两档:

  • 短内容(标题、评论、单行输入):300–500ms。用户停顿即意图明确,保存要快,否则"点了发布才发现没保存"的事故会找上你。
  • 长内容(文章正文、代码):800ms–2s。写长文时停顿是思考,不是写完。800ms 是个平衡点:既不会频繁打接口,又不会让"正在保存"转太久。

状态文案是 UX 的一半:编辑中… → 正在保存… → 已保存 12:03。三个状态缺一不可——"已保存"后面必须带时间,否则用户不知道"已保存"的是哪一版。AI 生成的代码经常只有"保存成功"的 toast,缺了时间戳,等于没说。

版本号冲突:多标签页同时编辑的灾难

用户在公司电脑和家里电脑同时打开同一篇草稿(或者只是开了两个标签页),两边都在写,最后保存的覆盖先保存的——last-write-wins,静默丢字。这是草稿系统里最阴险的 bug,因为它不报错,只是"字少了"。

解法是版本号乐观锁,上面代码里的 version 字段就是干这个的:

-- drafts 表
CREATE TABLE drafts (
  doc_id     TEXT PRIMARY KEY,
  content    TEXT NOT NULL,
  version    INTEGER NOT NULL DEFAULT 1,  -- 每次保存 +1
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 保存时带上"我基于的版本"
-- UPDATE drafts SET content = $1, version = version + 1
-- WHERE doc_id = $2 AND version = $3
-- 如果 affected rows = 0 → 说明有人先保存了 → 冲突!

冲突发生时,永远不要静默覆盖,要"保留两份让用户选":

  1. 把服务端的最新版和本地版都展示出来(diff 高亮更好,没有就并排);
  2. 给三个按钮:保留我的 / 用服务端的 / 合并后手动改;
  3. 无论选哪个,都记一条日志(谁在什么时候解决了哪次冲突)。

注意和实时协同的区别:协同编辑用 OT/CRDT 自动合并,因为那是"同时在线"的场景;草稿冲突是"先后离线"的场景,自动合并的风险远大于收益——让用户选,是唯一正确的 UX。

离线与弱网:移动端必做,桌面端别省

断网时用户还在写字(地铁、电梯、咖啡馆 Wi-Fi 抽风),你的保存链路不能断。方案是本地队列 + 恢复后按序同步:

// 离线队列:保存失败时入队,网络恢复后按序重放
const pendingQueue = [];

async function saveToServer(content) {
  try {
    const res = await api.put(`/docs/${docId}/draft`, { content, version });
    version = res.version;
  } catch (e) {
    // 网络失败:进队列,本地 localStorage 已有兜底,字不会丢
    pendingQueue.push({ content, version, ts: Date.now() });
    setSaveStatus('offline'); // "已离线,恢复后自动同步"
  }
}

window.addEventListener('online', async () => {
  // 按时间顺序重放,注意版本号可能已过期 → 走冲突流程
  for (const item of pendingQueue.splice(0)) {
    await saveToServer(item.content);
  }
});

三个细节:

  • 状态必须诚实:"已离线,恢复后自动同步"和"已保存"是两种状态,绝不能混。用户在离线时看到"已保存",等于你在骗他。
  • 重放可能触发冲突:离线期间,另一台设备可能已经保存了新版本。重放时带上旧 version,服务端一验就知道过期——直接走上一节的"保留两份"流程,不要尝试自动合并。
  • 队列要有上限:比如只保留最近 50 次。无限队列 + 长期离线 = 内存爆炸。超限时丢弃最老的,但 localStorage 里的完整快照还在,字不会丢。

草稿生命周期:出生、保留、善终

草稿不是写完就完,它有生命周期,三个问题要在设计时回答:

  1. 保留多久?建议:未发布的草稿保留 30 天,过期自动清理(参考软删除与回收站的策略:先标删除、7 天后真删,给用户反悔期)。已发布的文档,其草稿在发布成功后立即删除——留着就是垃圾。
  2. schema 复用:草稿表和正式内容表尽量用同一套 content 结构。AI 生成的代码常犯的错是草稿一套格式、正式内容另一套格式,发布时还要"转换"——转换就是 bug 的温床。
  3. 配额:一人团队初期不用限,但表结构里留个 user_id 维度的计数字段。等有人用草稿当网盘时,你才有手段限。

富文本/图片草稿特例

纯文本草稿好做,带图片的草稿有两个坑:

  • 没上传完的图片:用户粘贴了一张 5MB 的图,上传进度 30% 时关了页面。下次恢复草稿时,图片是个"上传中"的占位符——占位符必须能续传或重传,而不是裂图。实现:占位符里存本地 blob 的引用 ID,恢复时检查上传状态,未完成的重新触发上传。
  • blob URL 转正:粘贴图片时浏览器给的是 blob:https://... 临时 URL,关掉页面就失效。草稿保存时,必须把"已上传→转成永久 URL"这一步做完,草稿里只存永久 URL。这个"转正链路"和文件存储的上传链路复用,别自己再造一套。

Solo 上线检查清单

三个必测场景,测不过别上线:

  • ☐ 断网测试:写 500 字 → 断网 → 继续写 500 字 → 恢复网络 → 确认 1000 字全在,且状态文案经历了"已离线→正在保存→已保存"
  • ☐ 双标签页冲突测试:A 页写"111"保存,B 页(旧版本)写"222"保存 → 确认弹出"保留两份"选择,而不是静默覆盖
  • ☐ 崩溃恢复测试:写 500 字 → 直接杀浏览器进程 → 重开 → 确认提示"恢复草稿"且内容完整
  • ☐ 防抖参数按内容类型分档(短 300–500ms / 长 800ms–2s),"已保存"带时间戳
  • ☐ 草稿保留 30 天 + 发布后删除的生命周期已实现

草稿自动保存是那种"做好了没人夸,做砸了被骂死"的功能。但它衡量的恰恰是你的产品对用户时间的尊重——用户写下的每个字,都值得被认真对待。2000 字的信任,丢一次就很难找回来。

阅读 0评论 0

评论 (0)

ME
0/1000
评论加载中...
浏览项目广场发布你的项目

相关文章

深色模式切换开关 UI 示意图:深色胶囊按钮标有"Dark theme",带月亮图标开关
指南
别让深色模式闪瞎用户:vibe 项目的暗色模式与主题切换实战

AI 生成的暗色模式有三种固定套路的翻车:颜色写死散落各文件、切换时全屏闪白、系统偏好与用户选择打架。本指南给完整工程解法:语义化 CSS 变量(design token)作为唯一真相源、Tailwind v4 的 @custom-variant 接线、system/light/dark 三态状态机与 localStorage vs cookie 持久化规则、根治 SSR 首屏闪烁的 <head> 内联脚本(含 Next.js App Router 实操与量化验收),以及深水区——图片、canvas 图表、iframe 嵌入、WCAG AA 对比度抽查法,最后附硬编码颜色迁移的 5 步 SOP(含 AI 批量改写 prompt 与回归检查清单)。

工具技巧设计系统项目创作
实拍:程序员紧盯显示器上的代码逐行检查
指南
别再 JSON.parse 裸奔:AI 结构化输出的校验与修复实战

prompt 是建议,不是约束。AI 结构化输出可靠性的四层防线:模型原生结构化输出(OpenAI vs Anthropic,官方文档核实)、zod 校验的严格/宽松取舍、把报错原文喂回的修复重试、按成本选的降级兜底。

后端工程项目创作独立开发
实拍:工业控制面板上的红色急停按钮
指南
上线不靠祈祷:vibe 项目的功能开关与应急关停实战

功能开关把部署和发布解耦:代码先上,功能后开。四种开关类型与选型矩阵、一张表的最小实现、兜底铁律(新功能默认关、止血开关冻结)、开关腐烂治理,以及开关与灰度发布如何配合。

部署上线后端工程项目创作