欢迎光临
我们一直在努力

成为全栈·Next.js 网站前台篇·会员公开主页与通知中心:公开身份和私有消息如何分界

成为全栈·Next.js 网站前台篇·会员公开主页与通知中心:公开身份和私有消息如何分界

同一个会员既有可被搜索和分享的作者身份,也有只能本人读取的通知。前端不能因为数据都属于 User,就让公开主页和私有消息共享同一接口与缓存。

成为全栈·Next.js 网站前台篇·会员公开主页与通知中心:公开身份和私有消息如何分界

前言

内容站里的会员可能只是读者,也可能发表文章。公开作者页需要展示昵称和已发布文章,通知中心则包含评论审核、文章发布和账号消息。两者都与会员有关,公开范围却完全相反。

这篇通过两个页面说明数据分界:/members/[id] 是公开内容入口,可服务端渲染和索引;/member/notifications 属于当前会话,只能走私有请求和 no-store。

公开身份不是完整用户实体

字段公开作者页通知中心/账号接口
昵称、头像 可以 可以
已发布文章与数量 可以 可以按需引用
邮箱、账号状态 不展示 仅本人或管理权限
草稿、收藏、历史 不展示 仅本人
通知正文、未读状态 不展示 仅本人

最安全的契约是后端直接返回 PublicMember,而不是前端拿完整 User 再“记得删字段”。数据越早收窄,越不容易在日志、缓存或组件 props 中泄漏。

公开私有分界

公开作者页先校验路由身份

const { id } = await params
if (!/^\\d+$/.test(id)) notFound()

const member = await getMemberProfile(id).catch((error) => {
if (error instanceof ApiError && error.status === 404) notFound()
throw error
})

只有明确不存在才进入 404,网络错误继续由错误边界处理。非法 id 在请求上游前就被拒绝。

页面只渲染公开字段:

<section className="authorhero">
<div className="avatar large">{member.nickname?.slice(0, 1) || '读'}</div>
<div>
<span>作者主页</span>
<h1>{member.nickname}</h1>
<p>{member.articleCount} 篇公开文章</p>
</div>
</section>

{member.articles?.length ? (
member.articles.map((article) => (
<ArticleCard key={article.id} article={article} categories={tree} />
))
) : (
<Empty title="作者还没有发布文章" />
)}

没有公开文章是有效空状态,不代表会员不存在。草稿和待审核文章永远不应因为作者本人能看到,就混进公开主页。

通知查询键必须带身份、筛选与页码

const query = useQuery({
queryKey: ['notifications', user?.id, unread, page],
queryFn: () => getMyNotifications({
page,
pageSize: 10,
…(unread ? { isRead: false } : {}),
}),
enabled: !!user,
})

const count = useQuery({
queryKey: ['unread', user?.id],
queryFn: getUnreadCount,
enabled: !!user,
})

列表和未读数是两份服务端状态。任何标记操作成功后都要同时失效,否则标签显示“未读 3”,列表却已经全部已读。

标记已读与跳转要按顺序执行

const mutation = useMutation({
mutationFn: async ({ id, link }: { id?: number; link?: string }) => {
if (id) await updateNotification(id, true)
else await readAllNotifications()
return link
},
onSuccess: (link) => {
void client.invalidateQueries({ queryKey: ['notifications'] })
void client.invalidateQueries({ queryKey: ['unread'] })
if (link) window.location.assign(link)
},
})

先由服务端确认已读,再跳转详情。若先跳走,请求可能被中断,用户返回后仍看到未读。

“全部已读”后,未读筛选页可能突然没有当前页:

if (unread && !mutation.variables?.id) setPage(1)
else if (page > 1 && query.data?.list.length === 1) setPage(page – 1)

这与收藏末页删除是同一种分页恢复问题。

通知链接必须经过安全过滤

通知 link 来自服务端配置或业务事件,不能直接赋给 location.href:

export const safeLink = (value?: string | null): string | undefined => {
if (!value || /[\\\\\\r\\n]/.test(value)) return undefined
if (value.startsWith('/') && !value.startsWith('//')) return value

try {
const url = new URL(value)
return ['https:', 'http:'].includes(url.protocol) ? url.href : undefined
} catch {
return undefined
}
}

它拒绝 javascript:、协议相对地址和换行注入。当前允许 http(s) 外链;若产品只需要站内深链,可以进一步限制同源。

const link = safeLink(notification.link)
{link && (
<button onClick={() => mutation.mutate({ id: notification.id, link })}>
查看详情 →
</button>
)}

不要编造通知类型

const icon = notification.type === 'article_published'
? '文'
: notification.type === 'comment_approved'
? '评'
: '信'

前端只对契约已知类型提供特定图标,未知类型使用通用样式并显示服务端 title/body。不要根据正文关键词猜类型,也不要为尚未存在的“关注、私信”预造入口。

公开页与通知页使用完全不同的缓存路径

公开作者页:Server Component → serverFetch → 公开缓存
通知中心:Client Component → 同源 BFF → private, no-store

公开作者文章允许短暂陈旧,通知未读状态要求当前用户的新鲜结果。即使两页都显示昵称,也不能因此共用完整响应。

验收矩阵

场景公开作者页通知中心
游客访问 可读 跳转登录
会员无公开文章 显示空状态 不受影响
会员有草稿 不展示 相关审核通知可见本人
A 切换 B 公共页不敏感 清除 A 的通知缓存
全部标为已读 无变化 未读数和列表同步
通知 link 非法 无变化 不显示跳转按钮
后端 500 错误页,不是 404 局部失败可重试

通知时序

双账号测试时,应确认 B 看不到 A 的未读数、通知正文和分页缓存;公开作者页则在退出后仍可访问。这个对照能证明“公开身份”和“当前会话”没有被混为一谈。

适用边界

当前作者页只展示昵称与文章。如果以后增加简介、社交链接或关注关系,需要逐字段定义公开性与 URL 安全,不应直接复用个人资料表单模型。

通知量增大后可能需要游标分页、实时推送和批量操作。WebSocket 只负责告诉客户端“有变化”,最终未读状态仍应由服务端查询确认。

小结

会员是一个账号,公开作者和私有收件箱却是两个数据域。公开页只使用为公众设计的字段和文章,通知中心始终绑定当前用户、私有缓存和安全链接。

先确定信息是否公开,再选择渲染与缓存方式,比先复用一个 User 类型更可靠。

延伸阅读

  • 搜索页:实时查询、URL 状态与错误边界
  • 会员中心:资料、密码与个人数据

如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」:

🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html

📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer

成为全栈专栏订阅

赞(0)
未经允许不得转载:171主机测评 » 成为全栈·Next.js 网站前台篇·会员公开主页与通知中心:公开身份和私有消息如何分界
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址