本文介绍在 next.js 应用中(如 discord 克隆项目)如何解决“一个客户端修改数据后,其他客户端无法即时感知”的问题,涵盖轮询刷新(swr)、服务端预加载优化及 websocket 的适用边界。
本文介绍在 next.js 应用中(如 discord 克隆项目)如何解决“一个客户端修改数据后,其他客户端无法即时感知”的问题,涵盖轮询刷新(swr)、服务端预加载优化及 websocket 的适用边界。
在构建实时协作型应用(例如 Discord 克隆)时,一个常见痛点是:当用户 A 删除服务器后,用户 B 的界面仍显示该服务器,直到手动刷新页面——这显然违背了现代 Web 应用的实时体验预期。根本原因在于,客户端默认不具备“被动接收变更通知”的能力;传统 HTTP 请求是主动拉取(pull),而非服务端主动推送(push)。
对于大多数中低频实时场景(如服务器列表、频道状态、成员在线状态等),无需立即毫秒级同步,采用 @vercel/swr 的定时刷新策略是简洁、可靠且易于落地的首选方案。它避免了自建 WebSocket 服务的复杂性,同时比全量页面重载更轻量。
以下是一个典型的服务端列表组件示例:
"use client";import useSWR from "swr";// 假设 fetcherFn 是封装好的 API 调用函数async function fetcherFn(url: string) { const res = await fetch(url); if (!res.ok) throw new Error("Failed to fetch servers"); return res.json();}export default function Servers({ initialData }: { initialData: Server[] }) { const { data = initialData, isLoading, error } = useSWR<Server[]>( "/api/servers", fetcherFn, { refreshInterval: 30_000, // 每 30 秒自动刷新一次 fallbackData: initialData, // 优先渲染服务端预获取的数据,消除首屏空白 revalidateOnFocus: false, // 离开标签页时不重请求(可选) dedupingInterval: 2000, // 防抖重复请求(可选) } ); if (isLoading) return <div className="p-4">Loading servers...</div>; if (error) return <div className="p-4 text-red-500">Error loading</div>; return ( <ul className="space-y-2"> {data.map((server) => ( <li key={server.id} className="flex items-center gap-2 p-2 bg-gray-100 rounded"> <span className="font-medium">{server.name}</span> <button onClick={() => handleDelete(server.id)} className="text-xs text-red-600 hover:text-red-900" > Delete </button> </li> ))} </ul> );}
? 关键优化点:通过服务端组件(page.tsx)预先获取 initialData 并传入客户端组件,再交由 SWR 管理后续更新,既保障首屏速度与 SEO 友好性,又实现客户端状态的渐进式保鲜。
非真正实时:refreshInterval 决定了最终一致性延迟(如设为 30s,则最大延迟为 30s)。若业务要求“删除即刻消失”(如敏感操作审计、权限即时回收),则需升级至 WebSocket 或 Server-Sent Events(SSE)。
网络与性能开销:高频轮询会增加请求量。建议结合 revalidateIfStale: true 和 dedupingInterval 减少冗余请求,并对非核心列表(如历史消息)延长间隔或禁用自动刷新。
突变(Mutate)优于等待刷新:当用户本地触发变更(如点击删除),应立即调用 mutate() 手动更新缓存,实现视觉上的“瞬时响应”,再异步提交 API:
async function handleDelete(id: string) { // 1. 乐观更新:立即从 UI 和缓存中移除 mutate( "/api/servers", (prev: Server[]) => prev.filter(s => s.id !== id), { rollbackOnError: true } ); // 2. 异步提交删除请求 try { await fetch(`/api/servers/${id}`, { method: "DELETE" }); } catch (err) { // 失败时自动回滚(因设置了 rollbackOnError) console.error("Delete failed", err); }}
| 方案 | 实时性 | 复杂度 | 适用场景 |
|---|---|---|---|
| SWR / React Query 轮询 | 秒级延迟 | ★☆☆☆☆(极低) | 列表状态、非关键元数据同步 |
| WebSocket(如 Pusher、Supabase Realtime) | 毫秒级 | ★★★★☆(需鉴权、连接管理) | 聊天消息、在线状态、协同编辑 |
| Server-Sent Events (SSE) | 秒级内 | ★★☆☆☆(服务端需支持流) | 单向广播类通知(如系统公告) |
✅ 结论建议:从 SWR 轮询起步,覆盖 80% 的状态同步需求;当明确出现“用户投诉数据不同步”或产品文档要求“强实时”,再平滑迁移到 WebSocket —— 这是 Next.js 全栈应用最务实的演进路径。