ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

成为全栈·Next.js 网站前台篇·文章列表:服务端首屏与客户端持续加载怎样协作

2026/9/30 22:29:38 拓冰建站 浏览量
成为全栈·Next.js 网站前台篇·文章列表:服务端首屏与客户端持续加载怎样协作 成为全栈·Next.js 网站前台篇·文章列表服务端首屏与客户端持续加载怎样协作首屏内容适合由服务端直接交付继续加载需要浏览器管理交互状态。两者合在一起时真正难的是让第 2 页从第 1 页的下一步开始而不是重新发明一份列表。前言文章列表看起来是前端最熟悉的页面之一请求第 1 页循环渲染点击加载更多时请求第 2 页。可当第 1 页由 Server Component 获取后续页由 React Query 管理时列表同时有了两个执行环境。我们遇到的第一个问题不是请求失败而是登录状态恢复时清空整个 QueryClient。这个操作原本是为了防止上一个账号的私有数据残留却把正在展示的公开文章流也一起清掉。首屏已经渲染出来客户端接管后却突然回到加载状态。这个故障说明服务端首屏和客户端续页不只是两段代码它们共同管理一份用户正在阅读的数据。第 1 页是客户端查询的初始事实首页 Server Component 先请求最新 10 篇文章再传给Feedconst page await listArticles({ pageSize: 10, sort: -publishedAt }) return Feed initial{page} categories{tree} /Feed将这份结果作为“全部分类”查询的initialDatauseInfiniteQuery({queryKey:[public-feed,category],initialPageParam:1,initialData:category?undefined:{pages:[initial],pageParams:[1]},queryFn:({pageParam})request(/articles,{skipAuth:true,skipRefresh:true,query:{page:pageParam,pageSize:10,sort:-publishedAt,category},}),})这样 hydration 完成后浏览器不会立即再请求一次第 1 页来替换同样的内容。对服务端和客户端来说第 1 页是同一份初始事实客户端从它计算第 2 页。但切换到某个分类时不能继续使用“全部文章”的首页数据。queryKey包含 category分类非空时initialData变为undefinedReact Query 会为新键请求对应的第 1 页。否则切换页签时会短暂显示错误分类的文章。公开列表不应被会话恢复误伤公开文章不依赖当前账号。它的 query key 不应在登录、退出或恢复会话时被当作私有数据清理。当初直接调用queryClient.clear()虽然简单它实际上宣布了“所有客户端服务端状态都属于当前账号”。这和项目的数据所有权不符。更稳妥的方式是让会话切换只失效或移除me、favorites、notifications等私有 key保留public-feed、公开标签与文章摘要。这类错误之所以难发现是因为登录页和文章列表单独测都正常。只有“在首页滚动文章流同时恢复会话”这条组合路径才会暴露数据所有权判断错误。页码分页不是一份不变的快照文章 API 使用页码与 pageSize。当用户看完第 1 页之前如果后台又发布了新文章原第 1 页的最后一篇可能被挤到第 2 页。客户端接着请求第 2 页时就会再次收到这篇文章。因此展示前要按稳定 id 去重constitemsArray.from(newMap(pages.flatMap((page)page.list).map((article)[article.id,article]),).values(),)这段逻辑可以避免同一篇文章在页面里出现两次却无法保证零遗漏。若文章在分页间向前移动某篇还是可能在用户获取续页前跨过了页边界。要得到稳定快照接口需要游标、快照时间或稳定排序键支持不能由前端去重假装完成。所以本项目的承诺很明确防止重复展示不承诺动态发布过程中的绝对无遗漏。对博客文章流这个取舍可以接受对账单、订单和审计记录就应该改用更稳定的分页契约。自动加载要防止并发重入IntersectionObserver的回调可能在哨兵元素停留在视口时多次触发。isFetchingNextPage阻止前一页未返回时再发一次hasNextPage阻止越过最后一页query.isError让失败后的恢复交给明确按钮count 3则限制自动追加批次。这些条件不是“多写几个 if 更保险”而是四个独立不变量同一时刻只追加一页不请求不存在的下一页错误后不自动风暴重试页脚不被无限推走。下一页的计算也必须只相信服务端返回的分页信息getNextPageParam:(lastPage)lastPage.pagelastPage.pages?lastPage.page1:undefined不能用“这一页刚好有 10 条”推断还有下一页。总数恰好是 10 的倍数时它会多发一次服务端过滤或删除数据时也可能产生误判。观察器需要随查询状态正确建立与清理useEffect(() { const element sentinel.current if (!element || !hasNextPage || isFetchingNextPage || query.isError || autoPages 2) return const observer new IntersectionObserver(([entry]) { if (entry.isIntersecting) void fetchNextPage() }, { rootMargin: 240px }) observer.observe(element) return () observer.disconnect() }, [hasNextPage, isFetchingNextPage, query.isError, autoPages, fetchNextPage])rootMargin让请求在哨兵真正进入视口前启动减少读者看到加载空档的概率清理函数则防止分类切换后旧观察器继续触发。追加失败不应该删掉已读内容列表首次请求失败和第 3 页追加失败恢复策略并不相同。前者没有可展示内容重试时应该refetch()后者已经有两页可阅读内容页面应该保留它们只对失败的下一页再次调用fetchNextPage()。Failure retry{() { void (query.isFetchNextPageError ? fetchNextPage() : query.refetch()) }} /这条分支保护的是用户已经获得的阅读上下文。如果追加失败就把整个列表换成错误页用户不仅没拿到新内容连原来看到的也失去了。全部文章页为什么仍然保留传统分页首页文章流用于发现持续加载能降低每次点击翻页的中断感。全部文章页则用于定位用户可能要分享第 4 页、切换排序、返回原来位置搜索引擎也需要可追踪链接。因此/articles采用 Server Component 读取 URL 里的 page、sort 和 category渲染普通上下页链接。首页文章流底部也保留“按页浏览全部文章”入口。两种交互服务于不同任务没有必要为了界面统一二选一。nav aria-label文章分页 {page 1 Link href{withPage(page - 1)}上一页/Link} {visiblePages.map((number) ( Link key{number} href{withPage(number)} aria-current{number page ? page : undefined} {number} /Link ))} {page pages Link href{withPage(page 1)}下一页/Link} /nav服务端与客户端各自维护什么责任服务端首屏客户端续页第 1 页文章获取并写入 HTML作为initialData接管SEO 与无脚本可读负责不覆盖分类切换提供分类树query key 隔离并重新请求第 2 页以后不处理fetchNextPage()跨页重复无法预知后续变化按稳定 id 去重追加错误不处理保留已有页重试失败页验收时要覆盖“过程中发生变化”constscenarios[首屏水合后不重复请求第 1 页,第 2 页请求期间观察器重复触发,加载第 2 页前后台发布一篇新文章,第 3 页返回 500 后点击重试,自动追加两批后手动加载并抵达 Footer,会话从游客恢复为会员时公开列表仍保留,]静态数据库只能验证正常分页真正容易出错的是请求期间的发布、登录恢复与失败重试。把这些过程固定为回归清单才符合混合列表的实际风险。适用边界服务端首屏加客户端续页很适合公开内容流、商品列表和社区时间线。如果列表对绝对无重无漏有强要求需要先升级后端分页契约而不是在客户端继续加更多 Map 和补偿请求。如果列表数量很少或用户需要精确跳页传统分页仍然更简单。持续加载并不是更现代的默认答案它只是某种阅读任务的交互选项。小结这个列表的核心不是 React Query 或 IntersectionObserver而是服务端与浏览器对同一份数据达成了协议第 1 页是初始事实查询键表达筛选身份id 用于跨页去重追加失败不删除已有内容。各位看官做混合列表时先别急着写“加载更多”。先回答三个问题第 1 页由谁交付客户端用什么 key 接管数据在分页间移动时你能保证什么。延伸阅读内容门户首页多级分类、标签与 URLTanStack Query 不只是缓存失效、派生与失败恢复如果这篇文章对你有帮助欢迎订阅我的 CSDN 专栏「成为全栈」 专栏地址https://blog.csdn.net/fungleo/category_13204651.html 本系列配套代码仓库https://github.com/fengcms/become-a-full-stack-developer