
HY
折腾进行时
Hello, I'm HY.








留言板里一条评论下面挂着 13 条回复,往下翻要滑过整整一屏。评论区本来是用来交流的,被长回复串这么一撑,反倒成了劝退现场。
第一版我做的是「回复全部默认收起,只留一个展开按钮」。真跑起来一看,方向偏了:访客第一眼看到的评论下面全是空的,每条想看的都得先点一下,比滑屏还烦。
于是改成默认展示 2 条,多出来的才收着 —— 有两条内容垫底,一眼能看出这条评论有没有人聊过,长度也可控。还有剩就给个「展开更多(还剩 M 条)」,每次再放 5 条;全部摊开后按钮变「收起回复」,箭头翻上去,点一下回到默认的 2 条。

顶层评论保持原样、全部显示 —— 那是评论区的主体,收起来访客第一眼就什么都看不到了。要处理的是每条评论下面那一串回复。这条线划清楚之后,剩下的都是实现问题。
还有一条边界没得商量:回复本来就不超过 2 条的评论,我们完全不碰 —— 不折叠、不打标记、连按钮都不插,跟没这个功能一样。只在真正会长的地方动手,是让这个功能不惹人烦的前提。
| 文件 | 状态 | 干什么 |
|---|---|---|
src/config/FooterConfig.html | 改(追加) | 按钮样式 + PAGE_SCRIPTS 全站加载条目 |
public/assets/js/comment-reply-collapse.js | 新增 | 插按钮、切可见条数、盯着 DOM 变化 |
还是本站用惯的路子:页脚在 #swup-container 之外,切页不会被替换掉,所以样式写在它的 <style> 块里;脚本挂进 PAGE_SCRIPTS 全站加载(正则 /.*/,跟「最近评论」那条一样),没有 #waline 的页面自动空转。没有改任何主题源文件。
Waline 是 Vue 应用,DOM 不是写死的,动手前得先把渲染结果 dump 出来看。第一次抓就抓错了:#waline .wl-comment 看起来最像「一条评论」,其实它是编辑器面板(.wl-comment > .wl-panel,里面是昵称、邮箱、网址三件套加一个 textarea)。
真正的结构是这样:
1#waline2└── div3 ├── .wl-comment ← 编辑器(发评论那个框)4 └── div5 ├── .wl-meta-head ← 「25 评论」+ 按正序 / 倒序 / 热度6 ├── .wl-cards ← 评论列表7 │ └── .wl-card-item × 8 ← 顶层评论(id 就是评论的 objectId)8 │ ├── .wl-user ← 左侧头像列9 │ └── .wl-card10 │ ├── .wl-head ← 昵称 / 时间 / 点赞 / 回复11 │ ├── .wl-meta ← 归属地 / 浏览器 / 系统12 │ ├── .wl-content ← 正文13 │ └── .wl-quote ← 回复全挂在这里14 │ └── .wl-card-item × N15 └── .wl-meta-foot ← RSS 订阅 · Powered by Waline v3.16.1这里有两个必须先确认的事实。
一是回复不是嵌套的顶层评论,二级条目并不是 .wl-cards 的直接子节点,而是塞在每张卡片自己的 .wl-quote 里。友链页显示「25 评论」= 8 条顶层 + 17 条回复,其中只有 4 条评论带回复,最长的那条挂了 13 条。
二是没有回复时 .wl-quote 依然存在。8 条顶层评论对应 8 个 .wl-quote,但只有 4 个里面有东西。所以判定条件不能写成「有没有 .wl-quote」,得看它有没有 .wl-card-item 直接子节点。
Waline 里点「回复」会在原位插入一个编辑框,回复的回复也会往 .wl-quote 里塞条目。如果按 querySelectorAll(".wl-card-item") 一把捞,捞出来的可能混着深层嵌套的条目,层级就乱了 —— 数出来的「N 条回复」是错的,按序号切可见条数也会切错位置。
所以取子项一律走直接子:
1function directItems(quote) {2 var out = [];3 for (var i = 0; i < quote.children.length; i++) {4 var el = quote.children[i];5 if (el.classList && el.classList.contains("wl-card-item")) out.push(el);6 }7 return out;8}好处是那个编辑框压根不会被算进来,它没有 wl-card-item 这个类。按钮也一样,找它的时候只翻直接子,避免抓到深层同名元素。
显示控制也不走内联 style,而是加一个类,把样式留在页脚那份 CSS 里:
1.wl-quote > .ff-rp-hidden {2 display: none;3}不写内联是因为不想跟 Vue 自己的 style 绑定打架;选择器前面挂 .wl-quote > 是把优先级抬到 (0,2,0),稳过 Waline 自带的 .wl-card-item,同时又是未分层 CSS,也压得过主题 @layer 里的规则。
Waline 的渲染是异步的:#waline 这个容器先由 Astro 吐出来,评论列表要等客户端脚本从服务端拉完数据才长出来。而本站又套了 swup 无刷新切页,#waline 会随着页面容器被整个换掉。
如果观察器挂在 #waline 上,换页那一刻它监视的节点就没了,之后新渲染出来的评论区再也没人管 —— 表现就是「首页进去有折叠,站内点进文章页就没了」。挂在 document.body 上就没这问题,换页后重新长出来的节点照样能接住:
1new MutationObserver(schedule).observe(document.body, {2 childList: true,3 subtree: true,4});代价是回调会被全站所有 DOM 变动触发,所以加了 160ms 防抖,并且第一件事就是 getElementById("waline"),没有就直接返回。
这是最需要防的一条。折叠状态是我们手动打在 DOM 上的(一个按钮节点 + 一个 data-ff-shown),而 Waline 会在切换排序、提交评论、加载分页时重绘列表。节点被重建之后,我们插的按钮可能没了,dataset 也可能残留 —— 要是只认 dataset,就会出现「标记说处理过了、但按钮其实不在」,那条评论的回复从此永久失踪。
所以判定写成了双重条件:标了已处理,且按钮确实还在,才算处理过。任一条不满足就重新接一遍。
1// 按钮还在就说明这条已经接过了,别重复插2if (quote.dataset.ffRp === "1" && findButton(quote)) return;判定很糙,但很关键:
1// 回复本来就没超过默认条数:原样摊着,不插按钮,也不打标记2if (items.length <= DEFAULT_SHOWN) return;注意这里连标记都不打。打过标记的意思是「这条我处理过了,以后不用再看」,而短回复串是会变长的 —— 有人接着回两条,它就超了。留着未处理状态,下一轮扫描(Waline 重绘时会触发)就能把它接上,不用刷新页面。
代价是按钮位置会随回复内容高度浮动,不像固定在每张卡片底部那样整齐。换来的是评论区里只有真正长的地方才多出一行东西。
在本地 dev 上对着友链页跑了几轮:
友链页 8 条顶层评论,回复数分别是 2 / 13 / 1 / 0 / 0 / 1 / 0 / 0:
| 场景 | 结果 |
|---|---|
| 首屏渲染 | 只有那条 13 回复的插了按钮:「展开更多(还剩 11 条)」,前 2 条平铺;其余 7 条(回复 2 / 1 / 0 条)完全没碰 |
| 点「展开更多」 | 7 条,按钮变「还剩 6 条」 |
| 再点 | 12 条,变「还剩 1 条」 |
| 再点 | 13 条全部显示,变「收起回复」,箭头翻转 |
| 再点 | 回到默认的 2 条,变回「展开更多(还剩 11 条)」 |
| swup 无刷新切页(首页 → 友链页) | 按钮照常出现,无丢失 |
| 切换排序(整个列表重渲染) | 按钮数量不变、每条评论最多 1 个、无重复插入,回到默认 2 条 |

折叠的是噪声,不是内容 —— 想聊的人照样点一下就看全,不想看的人不用再被一条 13 连回复的串推着往下滑。
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!
部分内容可能已过时
分享你的想法,与大家交流讨论