昨天在群里有人吐槽我的博客评论区:辛辛苦苦写了一长串评论,手指一滑点错了按钮或者不小心按了返回键,页面刷一下,刚才写的东西全没了。这种体验确实挺糟心的,特别是在写长评论的时候。

于是我给评论区加了个草稿自动保存功能,写完提交之前,草稿会一直待在浏览器里。
需求很简单,方案也不复杂
要做评论草稿保存,其实可选择的方案就那么几种:
- Cookie:容量太小,不适合大段文本
- LocalStorage:持久化存储,关闭浏览器也不会丢
- SessionStorage:标签页级别的存储,关闭标签页就清空
- 后端存储:需要用户登录,而且服务端要处理匿名草稿的逻辑
最后我选了 sessionStorage。原因也很直接:草稿这种东西,本来就应该跟着标签页走。用户在 A 标签页写的草稿,不应该跑到 B 标签页去;关闭了标签页,下次重新打开,本来就不应该还有上次没写完的东西。
sessionStorage 的几个特点
用 sessionStorage 之前,我重新看了一遍它的行为,确认自己没有理解错:
1. 绑定在标签页级别
这是它和 localStorage 最大的区别。同一个域下的 localStorage,在所有标签页之间共享读写。而 sessionStorage 的数据只存在于当前标签页,关闭标签页或者关闭浏览器后,数据就没了。
2. 页面刷新不会丢失
这个很重要。用户在写评论的时候点了浏览器的刷新按钮,或者按 F5 刷新了页面,sessionStorage 里的数据依然在。这样才能做到真正的"草稿保存"。
3. 不会跨标签页同步
有些用户习惯开两个标签页看同一篇文章,这种情况下两个标签页的草稿是独立的,互不干扰。这个行为比较符合直觉——你在 A 标签页写的东西,B 标签页不应该知道。
4. 容量比 Cookie 大很多
虽然各家浏览器实现不太一致,但一般来说 sessionStorage 能存个 5MB 左右的数据,对于评论草稿来说绑绑有余。
实现细节
前端用的是 Alpine.js,组件注册函数大概长这样:
Alpine.data('commentDraftForm', (config = {}) => ({
storageKey: config.storageKey,
// 字段监听
startWatching() {
['content', 'name', 'email', 'website', 'parentId', 'replyingTo']
.forEach((field) => {
this.$watch(field, () => this.queuePersist());
});
},
// 防抖写入
queuePersist() {
window.clearTimeout(this.persistTimer);
this.persistTimer = window.setTimeout(() => this.persistDraft(), 240);
},
// 保存草稿
persistDraft() {
const payload = this.buildDraftPayload();
if (!this.hasPersistableData(payload)) {
this.clearDraft();
return;
}
window.sessionStorage.setItem(this.storageKey, JSON.stringify(payload));
}
}));
有几个点值得说一下:
防抖处理。用户打字的时候,如果每次输入都立即写入 sessionStorage,频繁的 IO 操作可能会影响输入体验。加个 240 毫秒的防抖,折中考虑了响应速度和性能。
回复目标的校验。评论草稿里会保存用户在回复哪条评论的信息(parent_id 和 replyingTo)。但如果用户从文章 A 切到文章 B,之前保存的回复目标在文章 B 里可能根本不存在。所以恢复草稿的时候,要校验这个回复目标是否在当前页面的可用列表里。
canRestoreReplyTarget(parentId, replyingTo) {
const normalizedParentId = this.normalizeReplyValue(parentId);
return normalizedParentId !== null
&& normalizedParentId === normalizedReplyingTo
&& this.availableReplyTargets.includes(normalizedParentId);
}
如果校验不通过,就清空回复目标,只恢复评论正文和用户信息。这样至少不会把回复状态搞混。
提交成功后清理草稿。这个不用多说,评论都发出去了,草稿留着也没用。
选 sessionStorage 还是 localStorage
当时也考虑过 localStorage,因为很多人第一印象觉得"持久化保存"更靠谱。但仔细想想,localStorage 的持久化反而会带来一些问题:
- 用户在 A 文章写了半截评论,关闭了标签页
- 第二天重新打开浏览器,发现草稿还在——这时候用户可能已经忘了这是什么时候写的
- 更重要的是,localStorage 会跨标签页共享。如果用户同时打开两篇文章,两边的草稿会互相覆盖
sessionStorage 的"标签页级别"特性,反而更符合用户对"草稿"的预期。
写在最后
这个功能做起来不算复杂,但确实改善了一个很实际的体验问题。有时候就是这样,不需要什么高深的技术,解决一个真实痛点就够了。
如果你也有类似的场景需要保存临时状态,可以先想想这个数据需不需要跨标签页共享。如果不需要,sessionStorage 基本上就是最优解。