前端开发工具【免费下载链接】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),仅供参考