首页/新闻资讯/正文详情

Relay Connections 与分页实战:从游标建模范式到 usePaginationFragment 无限滚动

发布时间:2026/9/23 20:49:24 来源:云帆数科 栏目:资讯中心
Relay Connections 与分页实战:从游标建模范式到 usePaginationFragment 无限滚动
前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载本文以 Relay v18 官方教程《Connections Pagination》为骨架系统讲解 Relay 处理大量集合数据的核心抽象——Connection连接从 schema 中为什么要用 边Edge 游标Cursor 建模范式到如何通过connection、refetchable指令与usePaginationFragmentHook 实现 加载更多 与无限滚动并辅以本仓库中relay-runtime、react-relay的真实源码佐证其底层机制。读完本文你将掌握一套完整、可直接落地的 Relay 分页实战方案。一、为什么需要 Connection三个必须理解的设计要点在 Relay 中分页列表与无限滚动都依赖一个名为Connection的 schema 建模范式。Relay 之所以能在分页场景下替你完成大量工作前提是你的 schema 遵循这套约定。这套约定来自多年构建集合类产品的经验理解它的设计动机比死记指令用法更重要。以下三个要点是全部机制的地基边Edge本身拥有属性。例如好友列表中你们成为好友的日期 是你与对方这条边上的属性而不是对方这个人的属性。因此需要用节点来表示边即 Edge 节点。列表本身拥有属性。例如是否还有下一页。因此需要一个表示列表本身的节点以及一个表示当前页的节点PageInfo。分页基于游标Cursor——服务端返回的不透明符号指向下一批结果的起始位置而不是基于 offset 偏移量。想象我们要展示用户的好友列表。从高层看这是一张图viewer 与每位好友各是一个节点viewer 到每位好友各有一条边而边本身携带属性接下来看如何用 GraphQL 建模这种情形。1.1 用节点表示边Edge 的诞生GraphQL 中只有节点能拥有属性边不能。所以第一步把你到好友这条概念边用一个专属节点表示此时边的属性由一个名为FriendsEdge的新类型表示。对应的查询长这样# XXX 仅示意非最终代码 fragment FriendsFragment1 on Viewer { friends { since # 边的属性你们成为好友的日期 node { name # 好友本身的属性 } } }since这种边特有的信息如成为好友的日期、在某条评论下的点赞时间等从此有了合适的存放位置。这是 Connection 范式对真实业务建模的第一个贡献。1.2 用节点表示列表Connection 的诞生要支持分页与无限滚动schema 必须满足三个客户端诉求客户端要能指定每页大小客户端要能得知是否还有更多页以便启用/禁用 下一页 按钮或在无限滚动场景下停止继续请求客户端要能请求已有结果之后的下一页。GraphQL 中指定页大小靠字段参数完成——查询不再是friends而是friends(first: 3)把页大小作为参数传给friends字段。而是否还有下一页这类信息属于列表本身为此需要像为每条边引入节点一样为列表本身引入一个节点这就是Connection 节点。它代表你和好友之间的这个连接本身元数据存放在这里——例如一个totalCount字段表示好友总数。此外它固定有两个字段表示当前页pageInfo当前页的元数据如是否还有下一页edges指向前面提到的那些边。1.3 用游标请求下一页在上图中可以看到PageInfo节点上有一个lastCursor字段——这是服务端返回的不透明令牌代表已给到的最后一条边示例中的好友 Charmaine在列表中的位置。把它作为参数回传给friends字段就能请求这些好友之后的好友这套分页列表建模范式的完整规范由 GraphQL Cursor Connections Spec 定义。它足够灵活以适配大量应用场景即便你不使用 Relay按这种约定设计 schema 也是一个好习惯——它让列表天然具备可分页性。二、实战为 Newsfeed 的评论实现 Load More Comments理解底层模型后我们把 Connection 用于真实功能为 Newsfeed 故事实现评论的 加载更多。先回到Story组件。有一个现成的StoryCommentsSection组件可以导入并放到Story底部import StoryCommentsSection from ./StoryCommentsSection; function Story({story}) { const data useFragment(StoryFragment, story); return ( Card Heading{data.title}/Heading PosterByline person{data.poster} / Timestamp time{data.posted_at} / Image image{data.image} / StorySummary summary{data.summary} / StoryCommentsSection story{data} / /Card ); }同时把StoryCommentsSection的 fragment 加入Story的 fragmentconst StoryFragment graphql fragment StoryFragment on Story { # ... 原有字段 ...StoryCommentsSectionFragment } ;再次运行npm run relay生成新的 Relay artifacts编辑器中的报错会随之消失。此时每个故事最多显示三条评论评论多于三条的故事会显示一个尚未接线的 Load more 按钮打开StoryCommentsSection看它的实现import LoadMoreCommentsButton from ./LoadMoreCommentsButton; const StoryCommentsSectionFragment graphql fragment StoryCommentsSectionFragment on Story { comments(first: 3) { edges { node { ...CommentFragment } } pageInfo { hasNextPage } } } ; function StoryCommentsSection({story}) { const data useFragment(StoryCommentsSectionFragment, story); const onLoadMore () {/* TODO */}; return ( {data.comments.edges.map(commentEdge Comment comment{commentEdge.node} / )} {data.comments.pageInfo.hasNextPage ( LoadMoreCommentsButton onClick{onLoadMore} / )} / ); }这里体现了 Connection 范式comments字段接收页大小参数每条评论对应一个edge其内部node承载真正的评论数据这里展开CommentFragment以便Comment组件渲染单条评论同时用pageInfo的hasNextPage决定是否显示 Load More 按钮。接下来让 Load More 真正生效。Relay 会处理底层细节但我们需要完成几步配置。2.1 增强 FragmentargumentDefinitions refetchable connection在改组件之前fragment 需要补充三块信息。第一步把页大小和游标抽成 fragment 参数不再硬编码const StoryCommentsSectionFragment graphql fragment StoryCommentsSectionFragment on Story argumentDefinitions( cursor: { type: String } count: { type: Int, defaultValue: 3 } ) { comments(after: $cursor, first: $count) { edges { node { ...CommentFragment } } pageInfo { hasNextPage } } } ;注意argumentDefinitions中的cursor没有defaultValue由 Relay 分页时自动传入而count的默认值是 3。第二步让 fragment 可 refetch这样 Relay 才能用新的参数值尤其是新的$cursor重新请求它const StoryCommentsSectionFragment graphql fragment StoryCommentsSectionFragment on Story refetchable(queryName: StoryCommentsSectionPaginationQuery) argumentDefinitions( cursor: { type: String } count: { type: Int, defaultValue: 3 } ) { # ... 同前 } ;关于refetchable的完整原理为什么需要它、它如何生成 refetch query可参考本仓库教程章节 refetchable-fragments。第三步用connection指令标记要分页的 Connection 字段const StoryCommentsSectionFragment graphql fragment StoryCommentsSectionFragment on Story refetchable(queryName: StoryCommentsSectionPaginationQuery) argumentDefinitions( cursor: { type: String } count: { type: Int, defaultValue: 3 } ) { comments(after: $cursor, first: $count) connection(key: StoryCommentsSectionFragment_comments) { edges { node { ...CommentFragment } } pageInfo { hasNextPage } } } ;connection指令需要一个key参数——必须是唯一字符串惯例是 fragment 名_字段名。这个 key 在后续用 mutation 增删连接内容时会用于定位详见本仓库教程后续章节 mutations-updates。源码印证为什么connection必不可少在运行时getPaginationMetadata.js 会从 fragment 节点的元数据中读取 connection 路径若缺失会直接触发 invariantExpected fragment ... to include a connection when using ... Did you forget to add a connection directive to the connection field in the fragment?。在编译器侧relay-codegen 的 connections 测试夹具 展示了connection(key: NodeQuery_comments)在编译产物中的解析形态包括嵌套 connectionfriends(first: 10) connection(key: NodeQuery_friends)的处理。2.2 改用 usePaginationFragmentfragment 准备好后改造组件把这两行const data useFragment(StoryCommentsSectionFragment, story); const onLoadMore () {/* TODO */};替换为const {data, loadNext} usePaginationFragment(StoryCommentsSectionFragment, story); const onLoadMore () loadNext(3);再次运行npm run relayLoad More 按钮现在会加载接下来的三条评论。usePaginationFragment位于packages/react-relay/relay-hooks/usePaginationFragment.js其完整返回值包括data、loadNext、loadPrevious、hasNext、hasPrevious、isLoadingNext、isLoadingPrevious、refetch。内部实现由两条useLoadMore分别负责 forward/backward 两个方向与底层的useRefetchableFragmentInternal组合而成其中 forward 方向即 加载下一页。loadNext 底层发生了什么追踪 useLoadMoreFunction.js 可以看到完整链路通过getConnectionState从当前 fragment 数据中解析出已有结果的末尾游标和hasMore状态调用getPaginationVariablesgetPaginationVariables.js合成分页变量——cursor与count由 Relay 自动填充且会校验调用方通过UNSTABLE_extraVariables传入的变量不得与游标/页大小冲突用createOperationDescriptor构造分页请求{force: true}强制重新请求再通过fetchQuery发起网络请求请求完成/出错时回调observer以重置isLoadingNext状态组件卸载时disposeFetch会取消在途请求。新边如何进入 store这正是 ConnectionHandler.js 的职责它作为 Connection 字段的默认运行时 handler在初始请求时把服务端返回的edges复制进按 handleKey 生成的客户端 connection 记录中后续每次分页请求则把新获取的边追加到既有连接末尾同时更新pageInfo的hasNextPage/endCursor等元数据——这正是无限滚动可以持续累积数据的根本原因。2.3 用 useTransition 改善加载体验目前的实现有一个体验问题点击 Load More 后在新评论加载完成前没有任何反馈。良好的交互要求每次用户操作都有即时反馈因此需要在加载期间显示 spinner同时保留现有 UI。做法是把loadNext的调用包进 React transitionfunction StoryCommentsSection({story}) { const [isPending, startTransition] useTransition(); const {data, loadNext} usePaginationFragment(StoryCommentsSectionFragment, story); const onLoadMore () startTransition(() { loadNext(3); }); return ( {data.comments.edges.map(commentEdge Comment comment{commentEdge.node} / )} {data.comments.pageInfo.hasNextPage ( LoadMoreCommentsButton onClick{onLoadMore} disabled{isPending} / )} {isPending CommentsLoadingSpinner /} / ); }原则是所有结果并非即时返回的用户操作都应包在 React transition 中。这使 React 能够对不同更新排定优先级——例如当数据返回、React 正要渲染新评论时用户点击了另一个 Tab 导航到其他页面React 可以中断评论渲染优先渲染用户真正想要的新页面。三、实战进阶Newsfeed 的无限滚动学完分页我们用同样思路打造无限滚动的 Newsfeed。它和 加载更多评论 几乎一样唯一区别是loadNext由滚动到页面底部自动触发而非点击按钮。Step 1 — 在查询中选中 Connection 字段当前应用用topStories根字段取前 3 条故事的简单数组。schema 在Viewer上还提供了newsfeedStories字段它就是一个 Connection。修改Newsfeed组件中的查询const NewsfeedQuery graphql query NewsfeedQuery { viewer { newsfeedStories(first: 3) { edges { node { id ...StoryFragment } } } } } ;我们把topStories换成viewer上的newsfeedStories用first参数先取前 3 条故事在edges内选择node它是一个Story节点所以能复用之前的StoryFragment同时选择id作为 Reactkey属性。提示虽然为了简化之前把topStory、topStories放在了Query顶层但按惯例与正在浏览页面/应用的人相关的字段应放在名为viewer的字段下。既然现在按真实应用的方式使用字段我们遵循这一惯例。Step 2 — 遍历 Connection 的 edges修改Newsfeed组件遍历 edges 并渲染每个 nodefunction Newsfeed() { const data useLazyLoadQuery(NewsfeedQuery, {}); const storyEdges data.viewer.newsfeedStories.edges; return ( {storyEdges.map(storyEdge Story key{storyEdge.node.id} story{storyEdge.node} / )} / ); }Step 3 — 把 Newsfeed 下沉到 FragmentRelay 的分页特性只作用于 fragment而非整个查询。原因在于虽然这个简单示例里直接在组件中发起了查询但在真实应用中查询通常由高层的路由组件发起而展示分页列表的往往是另一个组件。解决办法是把NewsfeedQuery的内容拆分到一个名为NewsfeedContentsFragment的 fragment 中const NewsfeedQuery graphql query NewsfeedQuery { ...NewsfeedContentsFragment } ; const NewsfeedContentsFragment graphql fragment NewsfeedContentsFragment on Query { viewer { newsfeedStories { edges { node { id ...StoryFragment } } } } } ;这里顺带说明每个 GraphQL schema 都包含一个Query类型代表查询可用的顶层字段。通过定义on Query的 fragment可以直接把它展开到查询顶层。组件内同时调用useLazyLoadQuery与useFragment真实项目中它们通常分处不同组件export default function Newsfeed() { const queryData useLazyLoadQueryNewsfeedQueryType(NewsfeedQuery, {}); const data useFragment(NewsfeedContentsFragment, queryData); const storyEdges data.newsfeedStories.edges; // ... }Step 4 — 为分页增强 Fragment现在既有 Connection 字段又有 fragment可以做与上一节完全相同的四件事为页大小和游标添加 fragment 参数first与after把参数作为字段参数传给newsfeedStories把 fragment 标记为refetchable用connection标记newsfeedStories字段。最终效果const NewsfeedContentsFragment graphql fragment NewsfeedContentsFragment on Query argumentDefinitions ( cursor: { type: String } count: { type: Int, defaultValue: 3 } ) refetchable(queryName: NewsfeedContentsRefetchQuery) { viewer { newsfeedStories(after: $cursor, first: $count) connection(key: NewsfeedContentsFragment_newsfeedStories) { edges { node { id ...StoryFragment } } } } } ;Step 5 — 调用 usePaginationFragment修改Newsfeed组件用usePaginationFragment替换useFragmentfunction Newsfeed() { const queryData useLazyLoadQueryNewsfeedQueryType( NewsfeedQuery, {}, ); const {data, loadNext} usePaginationFragment(NewsfeedContentsFragment, queryData); const storyEdges data.viewer.newsfeedStories.edges; return ( div classNamenewsfeed {storyEdges.map(storyEdge Story key{storyEdge.node.id} story{storyEdge.node} / )} /div ); }Step 6 — 用滚动触发器分页仓库为教程准备了一个InfiniteScrollTrigger组件用于检测是否滚动到页面底部并在合适时机调用loadNext。它需要知道是否还有更多页以及当前是否正在加载下一页——这两个信息都能从usePaginationFragment的返回值中取到import InfiniteScrollTrigger from ./InfiniteScrollTrigger; function Newsfeed() { const queryData useLazyLoadQueryNewsfeedQueryType( NewsfeedQuery, {}, ); const { data, loadNext, hasNext, isLoadingNext, } usePaginationFragment(NewsfeedContentsFragment, queryData); function onEndReached() { loadNext(1); } const storyEdges data.viewer.newsfeedStories.edges; return ( div classNamenewsfeed {storyEdges.map(storyEdge Story key{storyEdge.node.id} story{storyEdge.node} / )} InfiniteScrollTrigger onEndReached{onEndReached} hasNext{hasNext} isLoadingNext{isLoadingNext} / /div ); }现在滚动到页面底部就能看到更多故事持续加载——一个真实的 Newsfeed 应用体验。实现细节loadNext的第二个可选参数支持onComplete回调与UNSTABLE_extraVariables用于注入额外的分页变量但不能覆盖cursor/count否则 getPaginationVariables.js 会发出 warning。另外usePaginationFragment在开发模式下通过useDebugValue暴露fragment、data、hasNext、isLoadingNext等调试信息方便 React DevTools 排查。若想深入了解其类型签名可查看 usePaginationFragment.d.ts 与 hooks.d.ts。四、总结Connection 是 Relay 赖以对可分页列表建模范式的 schema 约定边Edge拥有属性用节点承载、列表本身Connection与当前页PageInfo拥有属性用节点承载、翻页基于不透明游标Cursor而非偏移量。只要 schema 遵循 Connection 约定无论是否使用 Relay都建议用 Connection 而非简单列表建模——这让列表天然具备可扩展分页的能力。实现分页的固定套路fragment 加argumentDefinitions参数 →refetchable(queryName: ...)→ 对 Connection 字段加connection(key: ...)→ 组件改用usePaginationFragment并调用loadNext。交互体验上把loadNext包进useTransition可让 UI 在加载期间保持响应无限滚动则用滚动触发器在hasNext为真时自动触发loadNext并用isLoadingNext防止重复请求。下一步我们将正式学习如何更新服务端数据。Connection 在那里同样扮演关键角色——我们会看到如何把新建的节点追加到既有 Connection 中对应教程 mutations-updates而运行时层正是由 ConnectionHandler.js 负责把这些边的增删落进本地 store。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay Connections 与分页实战从 Connection Schema 约定到 usePaginationFragment 无限滚动Relay Connections 与分页实战从 Connection Schema 约定到 usePaginationFragment 无限滚动 本指南以前端开发工具Relay 分页实战Connections 连接模型、usePaginationFragment 与无限滚动实现指南Relay 分页实战Connections 连接模型、usePaginationFragment 与无限滚动实现指南 Relay当前仓库 relay 在官前端开发工具Relay 教程Connections 与分页Pagination——从游标建模到 Load More 与无限滚动Relay 教程Connections 与分页Pagination——从游标建模到 Load More 与无限滚动 Relay 使用一种名为 Connec前端开发工具上一篇Plotly.py Sankey 桑基图绘制完全指南从基础流程图到节点、链接与布局的精细控制下一篇7个步骤打造专业视频会议界面Material-UI实时协作解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

科研文献高效检索与管理全攻略
科研文献高效检索与管理全攻略

1. 学术资源获取的痛点与解决方案作为一名在科研领域摸爬滚打多年的研究者,我深知查找国外期刊论文时那种"大海捞针"的无力感。记得刚开始做研究时,我常常花上整天时间在各大平台间切换,却找不到几篇真正相关的文献。直到后来掌握了… · 2026/9/23 20:49:18

重型颚式破碎机设计与优化关键技术解析
重型颚式破碎机设计与优化关键技术解析

1. 项目概述:重型颚式破碎机的工业价值复摆颚式破碎机作为矿山、建材、冶金等领域的核心破碎设备,其设计合理性直接影响生产线效率和运营成本。PE12001500这个型号代表进料口尺寸为1200mm1500mm,属于大型粗碎设备,每小时处理能力可… · 2026/9/23 20:49:18

learn-harness-engineering 之 harness-creator Skill 实战指南:为 AI 编码 Agent 构建、审计与评测生产级 Harness
learn-harness-engineering 之 harness-creator Skill 实战指南:为 AI 编码 Agent 构建、审计与评测生产级 Harness

【免费下载链接】learn-harness-engineering Harness engineering beginner tutorial, from 0 to 1 项目地址: https://gitcode.com/gh_mirrors/le/learn-harness-engineering 点击查看 免费下载 本指南以 learn-harness-engineering 仓库中随课程交付的 harness-c… · 2026/9/23 20:49:17

RDMA原子操作与Device Tracer实战:PRM第4分册避坑指南
RDMA原子操作与Device Tracer实战:PRM第4分册避坑指南

简介:这份资源是 Mellanox 网卡编程参考手册(PRM)第 4 部分,面向从事 RDMA 驱动开发、固件调试与高性能网络协议栈实现的工程师,以及需要深入理解 HCA 硬件行为的研究人员。内容聚焦扩展原子操作、WQE 格式与 RDMA 写原… · 2026/9/23 21:28:40

tvm.relay.nn:TVM Relay 神经网络算子库实战指南
tvm.relay.nn:TVM Relay 神经网络算子库实战指南

编译器深度学习模型优化 【免费下载链接】tvm Open deep learning compiler stack for cpu, gpu and specialized accelerators 项目地址: https://gitcode.com/gh_mirrors/tvm7/tvm 点击查看 免费下载 导读 tvm.relay.nn 是 Apache TVM Relay IR 中的神经网络算子… · 2026/9/23 21:28:40

SSM校园二手书交易系统:JSP+Servlet+MyBatis全链路实战
SSM校园二手书交易系统:JSP+Servlet+MyBatis全链路实战

简介:本资源是一套完整的Java毕业设计项目——基于SSM(SpringSpringMVCMyBatis)与JSP技术构建的校园二手书交易平台,面向计算机专业本科生及Java初学者,解决高校学生教材流转难、交易信息分散、平台可信度低等实际问题… · 2026/9/23 21:28:40

JAVA在线考试系统源码解析:Applet+Servlet+Btrieve环境搭建与改造
JAVA在线考试系统源码解析:Applet+Servlet+Btrieve环境搭建与改造

简介:JAVA在线考试管理系统源代码是一份面向高校计算机专业学生、Java开发初学者以及毕业设计开发者的完整项目资源。它围绕在线考试核心业务展开,涵盖试题库管理、考试安排、考生答题与成绩统计等关键模块,帮助读者理解从需求分析到系统实现… · 2026/9/23 21:28:39

VMD故障特征信号提取复现:变分模态分解、包络谱与排列熵实战
VMD故障特征信号提取复现:变分模态分解、包络谱与排列熵实战

简介:《基于VMD的故障特征信号提取方法》复现版MATLAB源码包,面向信号处理与机械设备故障诊断方向的初学者及研究人员。VMD即模态分解技术,能够将非平稳信号分解为多个频率局部化的模态分量,帮助从噪声中提取故障特征;… · 2026/9/23 21:28:33

Python学生成绩管理系统实战部署与避坑指南
Python学生成绩管理系统实战部署与避坑指南

简介:本资源是一套完整的Python学生成绩管理系统课程设计实践包,面向计算机专业初学者、课程设计学生及Python入门开发者,聚焦软件工程全流程实践,解决从需求分析到部署运行的系统开发能力训练问题。压缩包共412个文件&#xff0c… · 2026/9/23 21:28:33

3招搞定手机怎么下载微信面试难题实战项目解析
3招搞定手机怎么下载微信面试难题实战项目解析

3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03

你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型

你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29

Win7无线热点配置工具源码解析:解决API失效的3个实战技巧
Win7无线热点配置工具源码解析:解决API失效的3个实战技巧

Win7无线热点配置工具源码解析:解决API失效的3个实战技巧 Win7无线热点配置工具在Win10/11上跑不动?不是你的问题,是版本升级后 API 全变了。很多老项目里的 netsh wlan… · 2026/9/23 0:00:36

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码