☰
HowToGraphQL React+Relay 教程实践:用 PaginationContainer 实现游标分页
2026/9/25 3:07:00 网站建设 项目流程

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载

本文基于 HowToGraphQL(The Fullstack Tutorial for GraphQL)仓库中 React + Relay 教程的分页章节 整理。你将跟随该章节完成一个完整的实战目标:在教程的 Hackernews 克隆应用(React + Relay Modern,create-react-app构建,Graphcool 提供后端)中,把一次加载全部链接的列表,改造成底部带 “More” 按钮、按块追加加载的游标分页列表。读完并动手完成后,你将掌握 Relay Connection 规范(Edge/Node/cursor/pageInfo)、createPaginationContainer的完整配置对象、根查询变量透传,以及relay.loadMore / hasMore / isLoading的守卫式调用方式。

一、教程上下文:本章在做什么

该章节位于整条教程线的收尾阶段。在此之前,应用已经具备了:链接列表展示(第 2 章,使用createFragmentContainer+QueryRenderer)、创建链接与投票的 Mutation(第 3、6 章)、登录认证(第 5 章)、路由(第 4 章)和订阅实时刷新(第 7 章)。

分页一章的目标非常具体:让用户以更小、更可控的块来加载链接,而不是一次性拉取全部。实现形式是列表底部放一个More按钮,每点一次就向后端再取一块数据,追加到现有列表后面。

教程目录规划文件 meta/structure/react-relay.md 中,Pagination 一章规划的内容正是 “Limit/Offset vs Cursor、Relay Connections、Load Chunks of Links”,与本文件的正文完全对应,也说明了本章的侧重点:用游标分页替代传统的 limit-offset(“numbered pages”)分页。

一个值得注意的前置事实:由于本章要求服务端严格遵循 Relay Connection 规范,教程采用的后端是 Graphcool 的Relay API端点(https://api.graph.cool/relay/v1/<project-id>形式,见 Getting Started 章节),并且schema.graphql是完整下载后、针对当时 relay-compiler 的已知 bug 手工调整过的版本。这些环境细节决定了本套代码的适用前提。

另外,HowToGraphQL 站点本身用 Gatsby + React 渲染这些教程,正文中所有<Instruction>教学块由 Instruction 组件 负责渲染成高亮样式(它会筛选出P/PRE子节点分别套用instruction-block、instruction-code样式)。阅读线上版本时看到的高亮块,其实现就是这段源码。

二、Relay Connections:分页的数据结构基础

在 Relay 中,列表不是普通的数组,而是Connection(连接)抽象。它的目的是给一份简单的元素列表“增补”元信息——关于列表本身的信息,让客户端可以基于游标实现比 limit-offset 更稳的分页。

这也解释了为什么在本教程的早期章节里,每次访问列表元素都要做edges-node这一套“舞蹈”:Connection 不直接暴露元素,而是把每个元素包在Edge里,Edge携带该元素在列表中的上下文(位置,以及它前后相邻的部分)。

按 Relay Cursor Connections Specification,一个合格的 Connection,服务端需要满足:

  • 每个元素被包进Edge类型;
  • Edge至少暴露两个字段:
    • node:真正的元素数据;
    • cursor:该元素在列表中的位置。注意cursor是一个不透明(opaque)字符串——它不能也不应该在客户端自行生成,只能原样回传给服务端。
  • Connection 自身暴露pageInfo字段,其中包含四个子字段:
    • hasNextPage:布尔值,是否已到列表末尾(向前翻页时才有意义);
    • hasPreviousPage:布尔值,是否已到列表开头(向后翻页时才有意义);
    • startCursor/endCursor:本次返回的 edges 中第一条、最后一条对应的游标。
  • Connection 支持用于切片/分页的实参:
    • first和last:整型,用来切片、只取列表的一个子集;
    • before和after:字符串,接收游标,指定切片从哪里开始。
  • 此外,教程的后端 Graphcool 还在 Connection 上额外实现了count字段,用于直接查询当前列表中的元素总数。

最后一条实参正是分页能力的来源:first+after组合构成向前分页(forward pagination),last+before组合构成向后分页(backward pagination),二者都能精确取出列表中的一个具体块。而且 Relay 要求从 Connection 取数据时必须带上至少first或last之一(before/after可选)——Relay Compiler 会在编译期检查,缺失时直接报错。官方规范全文见文档引用的 Relay Cursor Connections Specification,文档中给出的相关讨论(Relay 仓库 issue #1201、graphql-relay-js issue #20)围绕的正是这一强制要求。

与第 2 章的衔接:当时的片段里allLinks(last: 100, ...)其实已经是合法的 Connection 取法(last满足“必须有 first 或 last”),只是写死了 100 条且没有after。本章把last: 100换成变量化的first: $count+after: $after,并保留同一个@connection(key: "LinkList_allLinks")——key 不变意味着 Relay Store 里的是同一条连接,新取到的块才能被正确追加而不是当成另一份数据。

三、PaginationContainerAPI

FragmentContainer(createFragmentContainer)你已经用过:组件 + 一个声明数据依赖的 GraphQL fragment,Relay 负责决定何时、如何取数。本章引入的是它的“分页增强版”——PaginationContainer:当数据来自 Connection 时,用它代替FragmentContainer,它会直接内置分页所需的便利方法。

PaginationContainer的前提同样是:服务端必须严格遵循 Connection 规范,因为整个实现依赖上面那些字段真实存在。

注入到组件 props 的relay对象上提供四个方法(原文档列出的完整清单):

方法返回作用
hasMoreboolean是否至少还有下一页可加载
isLoadingboolean由loadMore触发的一或多个请求是否仍在进行中
loadMore-加载当前连接的下一块数据,分页方向(forward/backward)由容器配置推断
refetchConnection-重新抓取连接中的数据(可带新的 variables)

这四个方法就是本教程整个_loadMore逻辑的全部依赖。

四、实现步骤一:为LinkList准备 “More” 按钮

和其他章节一样,先把纯 React 组件调好,数据获取逻辑后补。这里只需在LinkList组件底部加一个More按钮,并预留_loadMore方法桩。

LinkList.js的render调整为(高亮行为本章新增部分):

render() { return ( <div> <div> {this.props.viewer.allLinks.edges.map(({node}, index) => ( <Link key={node.__id} index={index} link={node}/> ))} </div> <div className='flex ml4 mv3 gray'> <div className='pointer' onClick={() => this._loadMore()}>More</div> </div> </div> ) }

注意两点细节:

  • this.props.viewer.allLinks.edges.map(...)仍是 Connection 的edges -> node访问模式,node通过__id(Relay 注入的内部唯一 id)作为 Reactkey;
  • 按钮绑定onClick={() => this._loadMore()},样式类来自教程在 Getting Started 章节 中引入的 Tachyons(pointer/flex/ml4/mv3/gray)。

然后在类中补上方法桩:

_loadMore() { // ... you'll implement this in a bit }

五、实现步骤二:用createPaginationContainer替换导出

这是本章的核心改动。打开LinkList.js,把原本的export default createFragmentContainer(...)整体替换为向createPaginationContainer传入组件 + fragment 配置 + 分页配置两部分:

export default createPaginationContainer(LinkList, { viewer: graphql` fragment LinkList_viewer on Viewer { allLinks( first: $count, after: $after, orderBy: createdAt_DESC ) @connection(key: "LinkList_allLinks") { edges { node { ...Link_link } } pageInfo { hasNextPage endCursor } } } `, }, { // ... this will be added soon } )

逐点解读这份 fragment:

  • first: $count取代了原来硬编码的last: 100。之所以用first,是因为要做的就是向前分页(从列表开头向末尾一块块推进);如果要做向后分页(从末尾往前取),则要改用last并在pageInfo里请求hasPreviousPage和startCursor。
  • after: $after:接收游标,指示列表从哪个位置开始切片。
  • orderBy: createdAt_DESC:保持按创建时间倒序,保证块与块之间的顺序稳定。
  • @connection(key: "LinkList_allLinks"):与第 2 章相同的连接 key,这是 Relay 在 Store 中定位、更新该连接所必需的。
  • pageInfo { hasNextPage endCursor }:向前分页只需要这两个字段。hasNextPage供hasMore判断,endCursor会被自动用于下一次请求的after。若实现向后分页则对应请求hasPreviousPage与startCursor。
  • fragment 命名遵循 Relay 约定<FileName>_<propName>:文件是LinkList,注入的 prop 是viewer,所以是LinkList_viewer;node里复用子组件的...Link_link片段,因为 Relay 要求父容器的 fragment 包含其子容器的全部数据依赖。

六、实现步骤三:第二个配置对象(分页行为本身)

把占位的{ ... this will be added soon }替换为完整配置:

{ direction: 'forward', query: graphql` query LinkListForwardQuery( $count: Int!, $after: String, ) { viewer { ...LinkList_viewer } } `, getConnectionFromProps(props) { return props.viewer && props.viewer.allLinks }, getFragmentVariables(previousVariables, totalCount) { return { ...previousVariables, count: totalCount, } }, getVariables(props, paginationInfo, fragmentVariables) { return { count: paginationInfo.count, after: paginationInfo.cursor, } }, }

各属性含义(原文档给出的完整说明,并补充适用细节):

  • direction:声明分页方向,取值只有forward或backward两种,必须与 fragment 中用的first/last一致(本例是forward)。
  • query:一条独立的查询,专门用于loadMore触发的所有请求(首次加载走的是QueryRenderer的根查询,二者互不干扰)。注意它的 variables 与 fragment 一致:$count: Int!(必填)、$after: String(可空,首页之后才有值)。
  • getConnectionFromProps:返回你要分页的那个连接。如果组件同时请求多个连接,就是靠它区分到底翻哪一个。
  • getFragmentVariables:返回读取 fragment 数据所用的变量。签名是(previousVariables, totalCount),这里把累计条数写进count,Relay 据此从 Store 中读出连接上已累积的元素。
  • getVariables:返回发送分页 query所用的变量。签名是(props, paginationInfo, fragmentVariables),其中paginationInfo.count是loadMore调用时传入的块大小,paginationInfo.cursor是 Relay 从pageInfo.endCursor中自动取出的下一块起点。

原文档特别提示:这份配置对象的官方文档很少,可靠的信息来源之一是react-relay包中ReactRelayPaginationContainer.js实现里的注释,建议结合实现源码阅读。

同时,react-relay的导入要同步调整:把createFragmentContainer换成createPaginationContainer(graphql的导入不变):

import { createPaginationContainer, graphql } from 'react-relay'

七、实现步骤四:根查询与QueryRenderer接收变量

因为$count是必填变量,getVariables/fragment 里用到的变量必须能一路传到组件树根部QueryRenderer的根查询。

打开LinkListPage.js,让根查询声明这两个变量:

const LinkListPageQuery = graphql` query LinkListPageQuery( $count: Int!, $after: String ) { viewer { ...LinkList_viewer } } `

再给QueryRenderer补上variablesprop,为首次请求提供count:

<QueryRenderer environment={environment} query={LinkListPageQuery} variables={{ count: ITEMS_PER_PAGE, }} render={({error, props}) => { if (error) { return <div>{error.message}</div> } else if (props) { return <LinkList viewer={props.viewer} /> } return <div>Loading</div> }} />

这里的environment是教程在 Getting Started 章节 中创建的 Relay Environment(relay-runtime的Environment+Network+Store,Network通过fetch指向 Graphcool 的 Relay API 端点)。

count指向一个应放在常量声明处的常量。在constants.js(第 5 章认证功能中已创建,存放GC_USER_ID、GC_AUTH_TOKEN等 key)中追加:

export const ITEMS_PER_PAGE = 1 // setting this only to one so you can easily test your pagination implementation

设为1是刻意为之:每页一条,点击 More 就能逐条追加,分页行为一目了然。实际项目中这里会是一个常规页大小。

然后回到LinkListPage.js导入它:

import {ITEMS_PER_PAGE} from '../constants'

八、实现步骤五:实现_loadMore并完成编译

最后一步,把第四章预留的桩方法填上。这是整条教程中_loadMore的最终形态:

_loadMore() { if (!this.props.relay.hasMore()) { console.log(`Nothing more to load`) return } else if (this.props.relay.isLoading()) { console.log(`Request is already pending`) return } this.props.relay.loadMore(ITEMS_PER_PAGE) }

三个要点:

  1. hasMore()守卫:pageInfo.hasNextPage为 false 时直接返回,避免发出无意义的请求——这直接消费了第二章 Connection 规范里的pageInfo元信息;
  2. isLoading()守卫:上一次loadMore的响应未返回时短路,防止并发追加导致连接状态错乱;
  3. loadMore(ITEMS_PER_PAGE):入参就是块大小,Relay 内部用它填充getVariables里的paginationInfo.count,并自动从endCursor取出after。

别忘了在LinkList.js顶部补上同一句导入:

import {ITEMS_PER_PAGE} from '../constants'

到这里,代码改动全部完成。由于新增了graphql标签代码(fragment 与LinkListForwardQuery),必须再跑一次 Relay Compiler 重新生成__generated__产物:

relay-compiler --src ./src --schema ./schema.graphql

(在hackernews-react-relay项目根目录执行;编译器会把生成物写入./src/__generated__,输出形如LinkList_viewer.graphql.js、LinkListPageQuery.graphql.js。)

随后yarn start启动应用:列表初始只加载一条链接,底部 More 按钮每点一次追加一条,直到hasMore返回 false 后点击只会在控制台打印 “Nothing more to load”。

九、要点回顾

  • Connection 是 Relay 分页的地基:Edge(node+ 不透明cursor)、pageInfo(hasNextPage/hasPreviousPage/startCursor/endCursor)、以及first/last+before/after实参,缺一不可;且必须至少提供first或last,否则 Relay Compiler 拒绝编译。
  • first+after= 向前分页,对应pageInfo里的hasNextPage/endCursor;向后分页则对称地使用last+before与hasPreviousPage/startCursor。
  • createPaginationContainer= 组件 + fragment 配置 + 分页配置;分页配置的五个关键项是direction、query(仅供loadMore使用)、getConnectionFromProps、getFragmentVariables、getVariables。
  • @connection的 key 要保持稳定(本例LinkList_allLinks),它决定新加载的块在 Relay Store 中如何与已有数据合并。
  • _loadMore的标准写法是双重守卫 +loadMore(块大小):先hasMore(),再isLoading(),最后relay.loadMore(ITEMS_PER_PAGE)。
  • 改动任何graphql标签代码后,都要重跑relay-compiler --src ./src --schema ./schema.graphql再启动应用。

适用前提说明:本套代码复现自 HowToGraphQL 的 React+Relay 教程线,依赖其特定环境——Relay Modern(react-relay 1.x 时代的 API)、Graphcool 的 Relay API 端点,以及为该环境准备过的schema.graphql。若在当前版本的 Relay 生态中落地,API 形态(如createPaginationContainer的写法与@connection用法)可能有差异,但 Connection 规范本身与本文讲的结构、分页参数(first/after、pageInfo)仍然成立。

本篇对应原始文档:content/frontend/react-relay/8-pagination.md,可结合同目录的 Queries 章节、路由章节 与 总结 以及 教程结构规划 一起阅读。

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载
上一篇:CANN ops-nn ForeachDivScalar 算子深度解析:基于 Ascend C 的 TensorList 标量除法实现与实战
下一篇:3步搞定视频封面与标签:Seal元数据处理全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询