评论实时更新:SSE 订阅文章频道,发评/编辑/删除/审核通过即时刷新打开中的页面

- internal/hub:按文章 ID 的进程内发布/订阅,信号不携带内容(未审核评论不外泄)
- GET /api/comments/stream?post_id=N:SSE 长连 + 25s 心跳 + X-Accel-Buffering off
- 广播点:发评论、作者编辑/删除、后台审核通过、后台删除(与公开端共享同一 hub)
- 前端 EventSource 静默刷新(不亮加载态、不干扰正在输入/编辑的状态)
- requestLog 的 statusRecorder 补 Flush 透传——包装器没实现 Flusher 时流式端点整体不可用
This commit is contained in:
Sakurasan
2026-09-28 21:42:11 +08:00
parent 661d157b6c
commit a234afe448
8 changed files with 168 additions and 2 deletions
@@ -1,5 +1,5 @@
<script setup>
import { computed, nextTick, onMounted, ref } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { site } from '../../site'
import { reader, loadReader, signIn, signOut } from '../../reader'
import { readerApi } from '../../api'
@@ -20,7 +20,19 @@ const props = defineProps({
})
const api = useComments(props.postId)
const { s, remaining, load, setSort, submit, saveEdit, remove, loadMoreReplies, startReply, startEdit, cancel } = api
const { s, remaining, load, refresh, setSort, submit, saveEdit, remove, loadMoreReplies, startReply, startEdit, cancel } = api
// 新评论实时出现:订阅这篇文章的 SSE 频道,后端广播「评论变了」就静默刷新。
// EventSource 自带断线重连;路由换文章时关旧开新。
let stream = null
function openStream() {
if (stream) stream.close()
if (!props.postId || typeof EventSource === 'undefined') return
stream = new EventSource(`/api/comments/stream?post_id=${props.postId}`)
stream.addEventListener('comments', () => refresh())
}
watch(() => props.postId, openStream, { immediate: true })
onBeforeUnmount(() => stream && stream.close())
// 总开关在站点设置里。后端阶段二才会下发 comments_enabled —— 在那之前整个
// 区块不渲染,线上访客不会看到一个请求 404 的半成品;后端一加就自动出现。
+19
View File
@@ -59,6 +59,24 @@ export function useComments(postId) {
load(true)
}
// SSE 触发的静默刷新:回到第一页按当前排序重拉,不亮加载态——
// 别人发了新评论时页面只是「悄悄长出」新内容,不该闪一下。
// 返回 true 表示列表真的变了(顶层条数有差异)。
async function refresh() {
try {
const data = await readerApi.comments(postId, { sort: s.sort, page: 1, size: PAGE_SIZE })
if (s.loading || s.loadingMore || s.replyTo || s.editing) return false
const items = (data && data.items) || []
const changed = (data && data.total) !== s.total || items.some((c, i) => !s.items[i] || s.items[i].id !== c.id || s.items[i].edited_at !== c.edited_at)
s.items = items
s.page = 1
s.total = (data && data.total) || 0
return changed
} catch {
return false
}
}
// 发表 / 回复。成功后直接插进列表,不整页重拉 —— 位置按当前排序决定,
// 用户刚发的东西必须马上看得见。
async function submit(bodyMd, parent = null) {
@@ -167,6 +185,7 @@ export function useComments(postId) {
s,
remaining,
load,
refresh,
setSort,
submit,
saveEdit,