如何在Next.js 13 App Router的Server Components中实现WordPress GraphQL分页?
我正在为WordPress网站构建Next.js前端,使用App Router和GraphQL插件获取数据。首页是Server Component,用来展示最新文章。当前代码能加载第一页文章,但无法在Server Component中实现基于GraphQL游标的“Load More”功能——因为Server Component不能使用useState/useEffect钩子,且WordPress GraphQL仅支持游标分页,之前尝试的查询字符串分页方案行不通。
解决方案:结合Server Component与Client Component实现分页
Server Component无法处理客户端交互,所以我们需要把分页的状态管理和点击事件交给Client Component,同时用Server Component获取初始数据,后续分页请求通过API Route或Server Action处理。
1. 调整GraphQL查询,获取分页关键信息
首先修改查询,让它返回pageInfo(包含下一页游标endCursor和是否还有更多内容hasNextPage),这样才能知道后续分页的起点和是否需要显示Load More按钮。
更新类型定义和getPosts函数:
interface Post { title: string; date: string; excerpt: string; featuredImage: { node: { sourceUrl: string; }; }; categories: { edges: { node: { name: string; }; }[]; }; slug: string; author: { node: { name: string; }; }; } interface PostsResponse { nodes: Post[]; pageInfo: { endCursor: string; hasNextPage: boolean; }; } async function getPosts(cursor: string): Promise<PostsResponse> { const endpoint = process.env.WORDPRESS_API_URL || ''; const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: ` query GetPosts($cursor: String!) { posts(first: 10, after: $cursor) { nodes { title date excerpt featuredImage { node { sourceUrl } } categories { edges { node { name } } } slug author { node { name } } } pageInfo { endCursor hasNextPage } } } `, variables: { cursor: cursor, }, }), }); const data = await response.json(); return data.data.posts; }
2. 编写Client Component处理分页交互
创建一个带'use client'指令的组件,负责管理分页状态、处理Load More点击事件,以及渲染文章列表。
'use client'; import { useState } from 'react'; import PostCard from './post'; import styles from './page.module.scss'; interface Post { title: string; date: string; excerpt: string; featuredImage: { node: { sourceUrl: string; }; }; categories: { edges: { node: { name: string; }; }[]; }; slug: string; author: { node: { name: string; }; }; } interface PostsPaginationProps { initialPosts: Post[]; initialEndCursor: string; hasNextPage: boolean; } export default function PostsPagination({ initialPosts, initialEndCursor, hasNextPage }: PostsPaginationProps) { const [posts, setPosts] = useState<Post[]>(initialPosts); const [currentCursor, setCurrentCursor] = useState(initialEndCursor); const [hasMore, setHasMore] = useState(hasNextPage); const [isLoading, setIsLoading] = useState(false); const loadMore = async () => { if (!hasMore || isLoading) return; setIsLoading(true); try { // 调用API Route获取下一页数据 const response = await fetch('/api/load-posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ cursor: currentCursor }), }); const data = await response.json(); setPosts(prev => [...prev, ...data.nodes]); setCurrentCursor(data.pageInfo.endCursor); setHasMore(data.pageInfo.hasNextPage); } catch (error) { console.error('加载更多文章失败:', error); } finally { setIsLoading(false); } }; return ( <div> <div className={styles.postsList}> {posts.map((post) => ( <PostCard post={post} key={post.slug} /> ))} </div> {hasMore && ( <button onClick={loadMore} disabled={isLoading}> {isLoading ? '加载中...' : '加载更多'} </button> )} </div> ); }
3. 创建API Route处理分页请求
在app/api/load-posts/route.ts中新建API路由,用来接收Client Component的请求,调用getPosts获取下一页数据:
import { NextResponse } from 'next/server'; import { getPosts } from '@/app/utils/getPosts'; // 建议把getPosts抽离到utils目录 export async function POST(request: Request) { const { cursor } = await request.json(); const postsData = await getPosts(cursor); return NextResponse.json(postsData); }
4. 修改首页Server Component
将首页的Server Component改为渲染上面的Client Component,传递初始的文章、游标和分页状态:
import PostsPagination from './PostsPagination'; import { getPosts } from './utils/getPosts'; export default async function HomePage() { const postsData = await getPosts(""); return ( <PostsPagination initialPosts={postsData.nodes} initialEndCursor={postsData.pageInfo.endCursor} hasNextPage={postsData.pageInfo.hasNextPage} /> ); }
替代方案:使用Server Actions简化流程
如果不想维护API Route,可以用Next.js的Server Actions直接在Client Component中调用Server端函数:
- 把
getPosts移到utils/getPosts.ts,并添加'use server'指令:
'use server'; // 这里是之前的PostsResponse、Post接口和getPosts函数
- 修改Client Component的
loadMore函数,直接调用Server Action:
'use client'; // ... 其他导入和接口定义不变 import { getPosts } from '@/utils/getPosts'; export default function PostsPagination({ initialPosts, initialEndCursor, hasNextPage }: PostsPaginationProps) { // ... 状态定义不变 const loadMore = async () => { if (!hasMore || isLoading) return; setIsLoading(true); try { const postsData = await getPosts(currentCursor); setPosts(prev => [...prev, ...postsData.nodes]); setCurrentCursor(postsData.pageInfo.endCursor); setHasMore(postsData.pageInfo.hasNextPage); } catch (error) { console.error('加载更多文章失败:', error); } finally { setIsLoading(false); } }; // ... 渲染部分不变 }
这样就省去了API Route的编写,代码更简洁。
内容的提问来源于stack exchange,提问作者Ioan

