GANKUDADIZ
BACK_TO_BLOG
TECH_LOG :: 2026.04.09

给评论区加个草稿箱:sessionStorage 的标签页级缓存

Avatar
By Gankudadiz · 1 min read · 33 views

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

于是我给评论区加了个草稿自动保存功能,写完提交之前,草稿会一直待在浏览器里。

需求很简单,方案也不复杂

要做评论草稿保存,其实可选择的方案就那么几种:

  • 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_idreplyingTo)。但如果用户从文章 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 基本上就是最优解。

评论 (4)

MuXiaoChen
MuXiaoChen 4 天前
很奇怪,我访问不到你的RSS地址
干枯大地z
干枯大地z 站长 / 管理员回复 3 天前
https://blog.gankudadiz.com/feed,这个地址是不是?我对这方面知识还不是特别懂,之前加入博客俱乐部的时候搞过这个页面,我就放首页rss按钮了
Mr.Lee
Mr.Lee 4 天前
emm...我是拿localStorage做存储的。但我是按照文章ID作为键存储的,不存在文章之间串台的问题。我没有多少篇文章,缓存太多评论占空间的问题我是暂时不用考虑的

> 第二天重新打开浏览器,发现草稿还在——这时候用户可能已经忘了这是什么时候写的

我认为这个不是啥大问题,`Ctrl+A` + `BackSpace`立刻就删了...

读了你的文章之后收获也很大,我并没有做防抖设计,确实并不优雅。
干枯大地z
干枯大地z 站长 / 管理员回复 4 天前
感谢大佬指出,果然博客的优化还需要更多用户的真实痛点来改进才对,自己看怎么都是ok的

[DRAFT_RESTORED]

已恢复你上次未提交的评论草稿。

草稿仅保留在当前标签页;提交成功后会清空,关闭标签页后会自动失效。

ACTION: