【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
本文基于 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对象上提供四个方法(原文档列出的完整清单):
| 方法 | 返回 | 作用 |
|---|---|---|
hasMore | boolean | 是否至少还有下一页可加载 |
isLoading | boolean | 由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) }三个要点:
hasMore()守卫:pageInfo.hasNextPage为 false 时直接返回,避免发出无意义的请求——这直接消费了第二章 Connection 规范里的pageInfo元信息;isLoading()守卫:上一次loadMore的响应未返回时短路,防止并发追加导致连接状态错乱;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
相关推荐
howtographql React & Relay 教程:用 Relay 指令式 API 实现投票 Mutation 与 Store 缓存更新
howtographql React & Relay 教程:用 Relay 指令式 API 实现投票 Mutation 与 Store 缓存更新 本文基于 Ho
HowToGraphQL React+Relay 实战:用 Relay 与 Graphcool 实现邮箱密码认证(Authentication)
HowToGraphQL React+Relay 实战:用 Relay 与 Graphcool 实现邮箱密码认证(Authentication) 本篇是 How
HowToGraphQL React 教程实战:用 Relay Modern 与 GraphQL Subscriptions 实现实时投票数更新
HowToGraphQL React 教程实战:用 Relay Modern 与 GraphQL Subscriptions 实现实时投票数更新 本文基于 Ho
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考