草稿保存优先用localStorage,因其简单同步、API直观、5MB容量足够单表单场景;IndexedDB对仅需按ID存取JSON的草稿属于过度设计,但需注意JSON序列化限制、高频写入抖动(应防抖)、时间戳校验、冲突提示及发布后精准清理。

草稿保存用 localStorage 还是 IndexedDB?
localStorage 足够应付大多数表单草稿场景,它简单、同步、API 直观,而且所有现代浏览器都支持。IndexedDB 虽然容量大、支持事务和复杂查询,但对“用户填一半关掉页面再回来”这种需求属于过度设计——你不需要查“上周三未提交的 200 字描述含‘紧急’的草稿”,只需要按表单 ID 存取一个 JSON 对象。
-
localStorage.setItem('draft-form-123', JSON.stringify(data)) 是最直接的写法,但要注意 JSON.stringify 无法序列化函数、undefined、Date 对象(会变成字符串)、RegExp 等;表单里若有自定义组件状态,得提前清洗
- 别在每次
input 事件里立刻写入 localStorage——高频触发会导致性能抖动,尤其在移动端;用 debounce 控制在 800ms 内无输入再保存更稳妥
- localStorage 有约 5MB 限制,但单个表单草稿极少超 100KB;真正要警惕的是同域下多个应用共用导致 quota 耗尽,建议加前缀如
'myapp-draft-form-'
怎么监听表单变化并提取有效数据?
不要遍历所有 input、textarea、select 手动取值——容易漏掉动态插入的字段、disabled 元素、富文本编辑器内容。推荐用事件委托 + 表单级序列化逻辑:
- 给表单加
data-draft-id="post-create" 属性,作为草稿 key 的来源
- 监听
form 元素的 input 和 change 事件(change 覆盖 checkbox/radio 的点击,input 覆盖实时输入)
- 提取数据时优先用
new FormData(form),它自动处理文件、多选、name 属性;但注意它不包含 disabled 字段,也不支持富文本区域(contenteditable 需单独读 innerHTML 或 innerText)
- 若表单含
quill 或 tiptap 编辑器,必须监听其特定事件(如 quill.on('text-change')),不能依赖原生表单事件
页面加载时如何安全恢复草稿?
恢复不是“有就塞进去”,而是要防冲突、防覆盖、防过期。核心是比对时间戳和用户意图:
- 草稿数据里必须存
savedAt 时间戳(Date.now()),加载时检查是否超过 7 天,过期则自动丢弃——避免用户半年后打开旧页面,误恢复早已作废的草稿
- 恢复前先判断当前 URL 或表单状态是否匹配:比如草稿是为
/admin/post/edit/456 存的,而用户打开的是 /admin/post/create,就不能自动填充
- 不要静默覆盖用户当前输入;显示提示条:“检测到未提交的草稿,是否恢复?”并提供“恢复”“清除并新建”两个按钮,用
confirm() 太粗暴,体验差
- 恢复后需重置草稿时间戳,否则下次保存会沿用旧时间,导致“刚恢复的草稿又被判定为过期”
发布成功后怎么清理草稿?
提交成功响应返回 201 或 200 后,清理动作必须与业务逻辑解耦,且确保只清对应草稿:
- 别在
fetch().then() 里直接 localStorage.removeItem()——网络请求成功不代表后端事务最终一致(比如发邮件失败回滚),应等后端明确返回“已持久化且不可逆”才清理
- 推荐在成功回调中调用统一清理函数:
clearDraft('post-create'),该函数内部检查 key 是否存在、是否匹配当前表单 ID,再移除
- 如果表单支持“另存为草稿”和“正式发布”两种按钮,注意“另存为”不应清空草稿,而“发布”必须清空;两者按钮的
type 和事件处理逻辑要严格区分
- 用户手动点击“清空草稿”时,应弹出二次确认,尤其当草稿非空且修改时间距今
草稿机制真正的难点不在存储,而在“什么时候不该恢复”和“什么时候不该清除”。时间戳、表单上下文、用户显式操作意图,这三者缺一不可。
localStorage.setItem('draft-form-123', JSON.stringify(data)) 是最直接的写法,但要注意 JSON.stringify 无法序列化函数、undefined、Date 对象(会变成字符串)、RegExp 等;表单里若有自定义组件状态,得提前清洗input 事件里立刻写入 localStorage——高频触发会导致性能抖动,尤其在移动端;用 debounce 控制在 800ms 内无输入再保存更稳妥'myapp-draft-form-'
input、textarea、select 手动取值——容易漏掉动态插入的字段、disabled 元素、富文本编辑器内容。推荐用事件委托 + 表单级序列化逻辑:
- 给表单加
data-draft-id="post-create"属性,作为草稿 key 的来源 - 监听
form元素的input和change事件(change覆盖 checkbox/radio 的点击,input覆盖实时输入) - 提取数据时优先用
new FormData(form),它自动处理文件、多选、name 属性;但注意它不包含 disabled 字段,也不支持富文本区域(contenteditable需单独读innerHTML或innerText) - 若表单含
quill或tiptap编辑器,必须监听其特定事件(如quill.on('text-change')),不能依赖原生表单事件
页面加载时如何安全恢复草稿?
恢复不是“有就塞进去”,而是要防冲突、防覆盖、防过期。核心是比对时间戳和用户意图:
- 草稿数据里必须存
savedAt 时间戳(Date.now()),加载时检查是否超过 7 天,过期则自动丢弃——避免用户半年后打开旧页面,误恢复早已作废的草稿
- 恢复前先判断当前 URL 或表单状态是否匹配:比如草稿是为
/admin/post/edit/456 存的,而用户打开的是 /admin/post/create,就不能自动填充
- 不要静默覆盖用户当前输入;显示提示条:“检测到未提交的草稿,是否恢复?”并提供“恢复”“清除并新建”两个按钮,用
confirm() 太粗暴,体验差
- 恢复后需重置草稿时间戳,否则下次保存会沿用旧时间,导致“刚恢复的草稿又被判定为过期”
发布成功后怎么清理草稿?
提交成功响应返回 201 或 200 后,清理动作必须与业务逻辑解耦,且确保只清对应草稿:
- 别在
fetch().then() 里直接 localStorage.removeItem()——网络请求成功不代表后端事务最终一致(比如发邮件失败回滚),应等后端明确返回“已持久化且不可逆”才清理
- 推荐在成功回调中调用统一清理函数:
clearDraft('post-create'),该函数内部检查 key 是否存在、是否匹配当前表单 ID,再移除
- 如果表单支持“另存为草稿”和“正式发布”两种按钮,注意“另存为”不应清空草稿,而“发布”必须清空;两者按钮的
type 和事件处理逻辑要严格区分
- 用户手动点击“清空草稿”时,应弹出二次确认,尤其当草稿非空且修改时间距今
草稿机制真正的难点不在存储,而在“什么时候不该恢复”和“什么时候不该清除”。时间戳、表单上下文、用户显式操作意图,这三者缺一不可。
savedAt 时间戳(Date.now()),加载时检查是否超过 7 天,过期则自动丢弃——避免用户半年后打开旧页面,误恢复早已作废的草稿/admin/post/edit/456 存的,而用户打开的是 /admin/post/create,就不能自动填充confirm() 太粗暴,体验差- 别在
fetch().then()里直接localStorage.removeItem()——网络请求成功不代表后端事务最终一致(比如发邮件失败回滚),应等后端明确返回“已持久化且不可逆”才清理 - 推荐在成功回调中调用统一清理函数:
clearDraft('post-create'),该函数内部检查 key 是否存在、是否匹配当前表单 ID,再移除 - 如果表单支持“另存为草稿”和“正式发布”两种按钮,注意“另存为”不应清空草稿,而“发布”必须清空;两者按钮的
type和事件处理逻辑要严格区分 - 用户手动点击“清空草稿”时,应弹出二次确认,尤其当草稿非空且修改时间距今



















