结论:仅在input事件中调用localStorage.setItem()最常见且最失效——中文输入法存拼音碎片、多表单共用key致覆盖、刷新后无法还原DOM、关页前未落盘;须加800ms防抖、动态键名、DOM就绪后派发input事件、beforeunload仅作提示不存数据。

直接说结论:只在 input 事件里调用 localStorage.setItem() 是最常见也最失效的做法——中文输入法下存的是拼音碎片,多表单共用 key 会互相覆盖,页面刷新后填不进 DOM,关页前最后一版根本没落盘。
监听 input 事件必须加防抖,且延迟 800ms
中文输入法(如搜狗、iOS 原生)在选词过程中会高频触发 input,例如打“中国”,实际会连续发出 “z”“zh”“zho”“zhon”“zhong”“zhongg”…… 直接写入会导致:localStorage 存了大量无效中间态;低端安卓机或 Safari 隐私模式可能静默拒绝写入;主线程阻塞造成卡顿。
手写防抖即可解决,不用引入 Lodash:
- 每次
input触发时先clearTimeout上一个定时器 - 仅当当前值与上次保存值不同时才执行
localStorage.setItem() - 延迟设为
800ms(比 300ms 更稳妥,适配输入法节奏) - 跳过
type="password"、data-no-autosave等字段
键名必须带上下文,不能用固定字符串如 "draft"
多个表单共用同一个 key(比如都用 "draft")是线上草稿“消失”的头号原因。浏览器同源下 localStorage 是全局共享的,A 表单一存,B 表单的草稿就没了。
立即学习“前端免费学习笔记(深入)”;
推荐键名格式:"draft-" + form.id + "-" + location.pathname,例如:
draft-contact-form-/contactdraft-article-456-/editor?id=456- 若表单无
id,可用form.dataset.formKey或生成短哈希(Date.now().toString(36))兜底
注意:动态渲染的表单(Vue/React/AJAX 加载),form.id 必须在 DOM 挂载后读取,不能写死在脚本开头。
恢复草稿必须等 DOM 真就位,并手动派发事件
很多代码在 DOMContentLoaded 里遍历 querySelectorAll('input, textarea') 并赋值,结果填了但框架没响应——因为 Vue 组件还没 mounted,React 表单还是受控状态,或者表单压根还没通过 AJAX 插进来。
安全做法是:
- 先检查元素是否存在:
if (el && el.value !== undefined) - 对
checkbox/radio设el.checked = Boolean(data[el.name]),不是赋value - 对
select匹配option[value]或设selectedIndex - 填完后必须调用
el.dispatchEvent(new Event('input', { bubbles: true })),否则v-model或useState不感知变化 - 恢复前校验时间戳:
Date.now() - data.savedAt < 24 * 60 * 60 * 1000,超时草稿默认不还原
beforeunload 只能提示,不能存数据
beforeunload 的执行窗口约 50ms,且强制同步——任何 await、fetch、甚至封装过的 saveDraft() 函数,只要内部含异步逻辑,都会失效。现代浏览器还屏蔽了自定义提示文案,只显示统一提示。
正确用法是:
- 在防抖保存成功后,把标志位
hasUnsavedChanges = false置为 false - 每次输入开始防抖时设为
true -
beforeunload回调里只做判断:if (hasUnsavedChanges) return '内容尚未保存,确定要离开?' - 真正存储动作必须已在之前完成,别指望它兜底
真正容易被忽略的是:移动端键盘收起不触发 blur,得配合 focusout 和 visibilitychange 补漏;Safari 无痕模式下 localStorage 被禁用,必须用 try/catch 包住写入,并降级到 sessionStorage 或内存缓存。



















