- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
本指南以 RedwoodJS 官方教程博客项目为基础,讲解如何在 RedwoodJS 全栈应用中为博客文章列表实现基于「页码(page)+ 偏移量(offset)」的经典分页方案:从 API 侧的 SDL 与 Service 编写、GraphQL 查询改造,到 Web 侧 Cell 的beforeQuery数据预处理与路由 query 参数透传,最终生成可点击跳转的Pagination组件。读完本文你将掌握一套可直接复制的 RedwoodJS 分页实现路径,并能理解其底层机制(服务层 Prisma 的take/skip、路由层的 query 解析与 Cell 生命周期钩子)。
分页方案概览:在 RedwoodJS 中如何组织一次「分页查询」
RedwoodJS 的典型数据流是:Web 侧的 Cell 发起 GraphQL 查询 → API 侧的 Service 解析器调用 Prisma Client 访问数据库 → 返回结果渲染到页面。分页场景下,我们需要在链路的两个环节分别扩展能力:
- API 侧:新增一个返回「当前页文章列表 + 文章总数」的 GraphQL 查询(如
postPage),并在 Service 中利用 Prisma 的take(每页条数)与skip(跳过的条数)实现偏移分页,同时用count()返回总数供前端计算总页数; - Web 侧:修改 Cell 的
QUERY与Success组件,并通过 RedwoodJS 路由的 query 字符串机制(?page=2)把页码从 URL 传递到页面组件,再透传给 Cell,最终在列表尾部渲染分页导航。
本指南将沿用官方教程(见 教程总览)创建的博客项目。假设你已有一个可运行的教程代码目录,并且数据库中至少有六篇帖子(本文按每页五条演示,六条即可产生两页)。
第一步:扩展 SDL,定义分页查询的类型结构
打开 API 侧的 SDL 文件(api/src/graphql/posts.sdl.js或 TypeScript 项目中的api/src/graphql/posts.sdl.ts),在现有Query类型中新增一个postPage查询。为了让前端能够渲染分页导航,查询结果不能只返回帖子数组,还要同时返回帖子总数,因此我们引入一个PostPage包装类型:
export const schema = gql` # ... type PostPage { posts: [Post!]! count: Int! } type Query { postPage(page: Int): PostPage posts: [Post!]! post(id: Int!): Post! } # ... `两个细节值得注意:
PostPage.posts使用[Post!]!非空数组,保证客户端一定能拿到数组(可能为空数组)而非null;postPage(page: Int)的page参数被设计为可选。这样客户端不传page时,服务端可以默认返回第一页,URL 无查询参数时页面依然可用。
第二步:在 Service 中实现分页解析器(Prisma take / skip / count)
打开api/src/services/posts/posts.js(或.ts),为postPage查询新增解析器。这是整个分页逻辑的核心:用take控制每页条数,用skip计算偏移量,并用count获取总数:
const POSTS_PER_PAGE = 5 export const postPage = ({ page = 1 }) => { const offset = (page - 1) * POSTS_PER_PAGE return { posts: db.post.findMany({ take: POSTS_PER_PAGE, skip: offset, orderBy: { createdAt: 'desc' }, }), count: db.post.count(), } }实现要点:
- 默认参数
page = 1:与 SDL 中参数可选的设计相呼应,未传page时回落到第一页; - 偏移量公式
offset = (page - 1) * POSTS_PER_PAGE:第一页跳过 0 条,第二页跳过 5 条,以此类推; take与skip:这是 Prisma Client 的分页基础能力,对应 SQL 层面的LIMIT/OFFSET;配合orderBy: { createdAt: 'desc' }保证分页结果顺序稳定,避免翻页时出现重复或遗漏;count并行获取总数:返回的count供前端计算总页数(Math.ceil(count / POSTS_PER_PAGE))。
完成这一步后,API 侧已经可以响应形如「第 2 页的 5 条帖子 + 总数」的 GraphQL 请求——Apollo 客户端发起查询,解析器经由 Prisma 从数据库取数,这正是 RedwoodJS 中「GraphQL 层 → Service 层 → 数据库层」的标准调用链。
第三步:改造 Cell 的 QUERY 与 Success 组件
Web 侧负责展示帖子列表的是BlogPostsCell(对应博客首页 HomePage)。首先把它的QUERY从原来的posts查询改为postPage查询,并把page作为查询变量传入:
export const QUERY = gql` query BlogPostsQuery($page: Int) { postPage(page: $page) { posts { id title body createdAt } count } } `如果是 TypeScript 项目,可以借助 RedwoodJS 自动生成的类型与TypedDocumentNode获得端到端类型安全:
import type { BlogPostsQuery, BlogPostsQueryVariables } from 'types/graphql' import type { TypedDocumentNode } from '@redwoodjs/web' export const QUERY: TypedDocumentNode<BlogPostsQuery, BlogPostsQueryVariables> = gql` query BlogPostsQuery($page: Int) { postPage(page: $page) { posts { id title body createdAt } count } } `查询结果结构从「帖子数组」变成了「包含posts和count的对象」,所以同文件中的Success组件也要同步调整解构方式:
export const Success = ({ postPage }) => { return postPage.posts.map((post) => <BlogPost key={post.id} post={post} />) }export const Success = ({ postPage, }: CellSuccessProps<BlogPostsQuery, BlogPostsQueryVariables>) => { return postPage.posts.map((post) => <BlogPost key={post.id} post={post} />) }第四步:利用路由 query 参数把 page 传给页面组件
现在的问题是如何把「当前页」这个值喂给查询。RedwoodJS 提供了一条捷径:URL 查询字符串中的参数会自动作为 props 传递给对应的页面组件。
回忆教程中的路由写法<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />——路径参数id会被作为 prop 传给BlogPostPage。查询字符串走的是同一套机制,但不需要修改任何路由定义:?page=2会被路由层解析后以pageprop 的形式注入页面组件。
这一点有源码可证:在 路由实现 中,路由层通过parseSearch(location.search)把location.search解析为对象,再与路径参数合并成allParams传给页面;而 parseSearch 实现 会遍历URLSearchParams的每个 key,产出{ key1: 'val1', key2: 'val2' }形式的普通对象。
因此HomePage只需声明pageprop 并向下传递:
const HomePage = ({ page = 1 }) => { return ( <BlogLayout> <BlogPostsCell page={page} /> </BlogLayout> ) }const HomePage = ({ page = 1 }) => { return ( <BlogLayout> <BlogPostsCell page={page} /> </BlogLayout> ) }行为说明:
- 访问
https://awesomeredwoodjsblog.com?page=2时,HomePage的pageprop 会被设为字符串"2",再原样透传给BlogPostsCell; - 没有
?page=时,page默认值为1(由组件默认参数兜底)。
第五步:用 beforeQuery 把字符串页码解析为数字
路由传来的page是字符串(例如"2"),而 GraphQL 变量需要数字。Cell 提供beforeQuery生命周期钩子,允许我们在查询执行前改写 props / 组装查询变量。在BlogPostsCell中补充:
export const beforeQuery = ({ page }) => { page = page ? parseInt(page, 10) : 1 return { variables: { page } } }export const beforeQuery = ({ page, }: FindBlogPostQueryVariables): GraphQLQueryHookOptions< FindBlogPostQuery, FindBlogPostQueryVariables > => { page = page ? parseInt(page, 10) : 1 return { variables: { page } } }从实现层面看,beforeQuery是 RedwoodJS Cell 工厂的内置扩展点:在 createCell 的实现 中,Cell 默认把「传入的 props 直接当作 GraphQL 变量」构造查询选项;当自定义beforeQuery存在时,则以你的返回值(即{ variables })替代默认行为,再交由useQuery执行。因此这里把字符串page用parseInt(page, 10)转成数字后放入variables.page,查询就会以数字参数请求postPage。
第六步:本地验证分页效果
执行yarn rw dev启动开发服务器(默认端口 8910):
- 访问 http://localhost:8910,应只看到前五条帖子(第一页);
- 把 URL 改为 http://localhost:8910?page=2,应看到后续五条帖子(若总共只有六条,则此处只显示一条)。
这验证了「URL query → 页面 prop → Cell 查询变量 → GraphQL 请求 → 数据库偏移查询」整条链路已打通。
第七步:生成并实现 Pagination 分页组件
最后添加一个可点击跳转的页码选择器。用 RedwoodJS CLI 生成组件骨架:
yarn rw g component Pagination然后实现如下逻辑(核心是count除以每页条数向上取整得到总页数,逐页生成Link):
import { Link, routes } from '@redwoodjs/router' const POSTS_PER_PAGE = 5 const Pagination = ({ count }) => { const items = [] for (let i = 0; i < Math.ceil(count / POSTS_PER_PAGE); i++) { items.push( <li key={i}> <Link to={routes.home({ page: i + 1 })}> {i + 1} </Link> </li> ) } return ( <> <h2>Pagination</h2> <ul>{items}</ul> </> ) } export default Paginationimport { Link, routes } from '@redwoodjs/router' const POSTS_PER_PAGE = 5 const Pagination = ({ count }: { count: number }) => { const items = [] for (let i = 0; i < Math.ceil(count / POSTS_PER_PAGE); i++) { items.push( <li key={i}> <Link to={routes.home({ page: i + 1 })}>{i + 1}</Link> </li> ) } return ( <> <h2>Pagination</h2> <ul>{items}</ul> </> ) } export default Pagination关键点:
routes.home({ page: i + 1 }):RedwoodJS 的命名路由函数接受一个参数对象,其中的键会被序列化为 URL 查询字符串,生成/ ?page=2这样的地址。点击某个页码链接后,浏览器地址变为?page=N,路由层解析 query → 页面 prop → Cell 变量,完成翻页;Math.ceil(count / POSTS_PER_PAGE):用总数除以每页条数向上取整,得到总页数并据此渲染对应数量的页码项;- 组件保持无样式,符合官方教程「不加 CSS」的风格;若想美化,只需去掉列表项前的圆点并改为水平排列即可。
第八步:把 Pagination 接入 BlogPostsCell
最后在BlogPostsCell的Success组件末尾渲染<Pagination>,并记得在文件顶部导入它:
import Pagination from 'src/components/Pagination' // ... export const Success = ({ postPage }) => { return ( <> {postPage.posts.map((post) => <BlogPost key={post.id} post={post} />)} <Pagination count={postPage.count} /> </> ) }import Pagination from 'src/components/Pagination' // ... export const Success = ({ postPage, }: CellSuccessProps<BlogPostsQuery, BlogPostsQueryVariables>) => { return ( <> {postPage.posts.map((post) => ( <BlogPost key={post.id} post={post} /> ))} <Pagination count={postPage.count} /> </> ) }至此,博客首页在帖子列表尾部就会出现「Pagination」标题与一组数字页码,点击即可跳转对应页面。
已知局限与改进方向
当前实现存在一个明显的技术局限:它对「页数很多」的场景处理得不够优雅。假设共有 100 页,列表会一次性渲染 100 个页码链接。官方文档把它留作读者练习——一个更完善的分页组件通常需要:
- 加入「上一页 / 下一页」按钮,并处理第一页 / 最后一页的禁用态;
- 显示窗口化的页码(如
1 … 4 5 6 … 20),避免页码爆炸; - 用
useMatch或当前pageprop 高亮当前页; - 可选地借助 Prisma 的游标分页(cursor-based pagination)替代偏移分页,应对深分页性能问题。
相关资源与进一步阅读
- 本教程构建于官方教程之上,前置基础见 教程总览;
- 完整的本篇内容(含 JS/TS 双版本代码)见 官方 Pagination 指南,最新版位于 docs/docs/how-to/pagination.md;
- 想深入理解 Cell 机制(
beforeQuery、afterQuery、Loading/Failure/Empty/Success生命周期),可阅读 createCell 源码; - 路由如何解析查询字符串并注入页面 props,见 router.tsx 与 parseSearch 实现;
- 分页相关的数据库与客户端原理可参考 Prisma 的分页文档与 Apollo 的分页文档(链接见原文档末尾)。
- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
相关推荐
终极解决TranslucentTB启动失败:VCLibs依赖修复完全指南
终极解决TranslucentTB启动失败:VCLibs依赖修复完全指南 TranslucentTB作为Windows任务栏透明化工具,因其轻量级和美观效果备受
后端前端Web框架开发工具RedwoodJS 分页实战:从 GraphQL 查询到前端分页组件的完整实现
RedwoodJS 分页实战:从 GraphQL 查询到前端分页组件的完整实现 本篇技术指南基于 RedwoodJS 官方文档 docs/docs/how to
后端前端Web框架开发工具RedwoodJS 分页实战:从 GraphQL SDL 到 Prisma 查询的完整实现指南
RedwoodJS 分页实战:从 GraphQL SDL 到 Prisma 查询的完整实现指南 导读 本文基于 RedwoodJS 官方教程( tutorial
后端前端Web框架开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考