Next.js搭配Apollo栈用JS slice实现分页是否合理?正确方案是什么?
现有实现的可行性和存在的问题
你目前通过前端slice做分页的方案在博客文章总数只有几十上百篇的小数据量场景下是完全可用的,切换页码无网络请求,响应速度更快,实现逻辑也更简单。
但数据量上涨之后这个方案的缺陷会非常明显:
- 你当前在
getStaticProps中一次性拉取全量文章数据,如果后续文章涨到几千甚至上万篇,接口返回的数据包会非常大,首次加载速度会严重变慢,用户体验极差 - 服务端和数据库压力大,每次ISR重新生成页面都要从MongoDB拉取全量文章,查询开销很高
- 不符合GraphQL按需取数的设计理念,浪费带宽和服务端资源
适配你技术栈的正确分页方案
你用的是Apollo全家桶+MongoDB,推荐改用后端分页方案,改造分几步:
1. 改造Apollo Server端的Schema和Resolver
博客场景用Offset分页足够简单好维护,先调整GraphQL查询定义,增加分页参数和总条数返回:
type Query { getPosts(page: Int!, limit: Int!): PostPageResult! } type PostPageResult { posts: [Post!]! total: Int! # 总文章数,用来计算总页数 }
Resolver层对接MongoDB的skip和limit实现分页查询:
// Resolver示例代码 Query: { getPosts: async (_, { page, limit }) => { const skipCount = (page - 1) * limit // 按发布时间倒序,返回对应页的文章 const posts = await Post.find().skip(skipCount).limit(limit).sort({ createdAt: -1 }) // 查询总文章数 const totalCount = await Post.countDocuments() return { posts, total: totalCount } } }
2. 改造前端查询逻辑
首先修改你的GraphQL查询语句,添加分页变量:
# lib/query.ts 里的查询修改 query GetPosts($page: Int!, $limit: Int!) { getPosts(page: $page, limit: $limit) { posts { _id title createdAt tags } total } }
首页逻辑可以根据你的需求选两种实现:
方案A:客户端动态查询(灵活,适合内容更新较频繁的博客)
用Apollo Client的useQuery做客户端查询,切换页码自动拉取对应页数据,你之前写的Pagination组件完全不用改,可以直接复用:
// /pages/index.tsx 核心逻辑修改后 import type { NextPage } from 'next' import Link from 'next/link' import Head from "next/head"; import { useQuery } from '@apollo/client' import { GET_POSTS } from "../lib/query"; import {useState} from "react"; import Pagination from "../components/pagination"; const Home: NextPage = () => { const [currentPage, setCurrentPage] = useState(1); const postsPerPage = 10 // 分页查询 const { loading, error, data } = useQuery(GET_POSTS, { variables: { page: currentPage, limit: postsPerPage } }) if (loading) return <p>加载中...</p> if (error) return <p>文章加载失败</p> const { posts, total } = data.getPosts return ( <div> <Head> <title>...</title> </Head> <h1 className="text-gray-550 dark:text-white">全部文章({total})</h1> <div className="flex items-center flex-col justify-center list-none"> {posts.map((post : any) => ( <Link href={`/posts/${post._id}`} key={post._id}> <a className="group container mt-5 p-4 border-b-2"> <h3 className="text-black text-xl dark:text-white mt-4 group-hover:text-purple-500">{post.title}</h3> <p className="text-black dark:text-white mb-4">{renderDate(post.createdAt)}</p> {renderTags(post.tags)} </a> </Link> ))} <Pagination postsPerPage={postsPerPage} totalPosts={total} paginate={setCurrentPage}/> </div> </div> ) } export default Home
方案B:保留静态生成优势(适合内容更新不频繁的博客)
可以新增动态路由/pages/page/[page].tsx,用getStaticPaths预生成前几页的静态页面,搭配ISR自动更新,兼顾性能和SEO。
可选优化
如果后续你的文章量超过万级,可以把Offset分页换成Cursor分页,用_id或者createdAt作为游标查询,避免MongoDB的skip在大数据量下性能衰减的问题。
内容的提问来源于stack exchange,提问作者JeremyChang
相关产品推荐
相关产品推荐

