Relay Fragments 完整指南:useFragment、组合与数据遮罩的实战解析
前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载Relay 是用于构建数据驱动 React 应用的 JavaScript 框架而 GraphQL Fragment 是它在组件层声明数据依赖的核心构建单元。本指南以 guided-tour/rendering/fragments.md 为主线系统讲解 Fragment 的声明方式、useFragmentHook 的渲染机制、父子组件间的 Fragment 组合、以及如何将 Fragment 挂载到查询之下并结合本仓库的 React 运行时源码与 Relay 编译器类型生成实现帮助你真正理解声明式数据依赖背后的原理并在实战中写出类型安全、可局部推理的组件代码。认识 Fragment组件的数据依赖单元在 GraphQL 中Fragment 是可复用的查询单元它表示要从 Schema 暴露的某个 GraphQL 类型上查询的一组数据。在 Relay 语境下Fragment 本质上就是对某个 GraphQL 类型的一组字段选择field selectionfragment UserFragment on User { name age profile_picture(scale: 2) { uri } }要在 JavaScript 代码中声明 Fragment必须使用graphql标签tag。它会触发 Relay 编译器的静态解析编译器随后为每个 Fragment 生成对应的产物文件与类型定义const {graphql} require(react-relay); const userFragment graphql fragment UserFragment_user on User { name age profile_picture(scale: 2) { uri } } ;关于 Fragment 命名官方约定为module_name_property_name模块名 下划线 属性名例如UserFragment_user。这样做有两个目的一是保证 Fragment 名全局唯一Relay 要求 Fragment 名称在整个应用中唯一因为编译器会按名称建立依赖图二是方便定位——看到UserComponent_user就知道它定义在UserComponent模块中即便同一个模块内定义了多个 Fragment 也不会冲突。注意graphql标签本身并不会执行任何网络请求它只负责登记这段数据依赖声明。真正的数据获取发生在 Fragment 被某个查询引用之后。使用 useFragment 渲染 Fragment 数据要渲染一个 Fragment 对应的数据需要使用useFragmentHook。它接收两个参数Fragment 定义graphql标签的产物和Fragment 引用fragment reference并返回该 Fragment 在当前引用下对应的dataimport type {UserComponent_user$key} from UserComponent_user.graphql; const React require(React); const {graphql, useFragment} require(react-relay); type Props { user: UserComponent_user$key, }; function UserComponent(props: Props) { const data useFragment( graphql fragment UserComponent_user on User { name profile_picture(scale: 2) { uri } } , props.user, ); return ( h1{data.name}/h1 div img src{data.profile_picture?.uri} / /div / ); } module.exports UserComponent;这里有几个关键概念需要展开useFragment的输入输出它接收 Fragment 定义和 Fragment 引用返回对应的数据。这与usePreloadedQuery的模式一致——后者接收查询定义和查询引用PreloadedQuery。在 useFragment.js 中useFragment会先通过getFragment(fragment)解析出ReaderFragment节点并调用useStaticFragmentNodeWarning校验 Fragment 必须是静态节点然后才把控制权交给useFragmentInternal。Fragment 引用是什么UserComponent_user这个 Fragment 本身只声明了User类型上的字段但我们需要知道具体要读哪一个User 的数据这正是 Fragment 引用所承载的信息。简单理解Fragment 引用就像一个指向某个具体类型实例的指针。在类型层面它对应生成的$key类型见下文。从源码看useFragmentInternal会用getSelector(fragmentNode, fragmentRef)把Fragment 节点 引用转换成ReaderSelector——见 RelayModernSelector.js当fragmentRef为null时直接返回null对应bailout状态否则根据 Fragment 是否标记为 plural 生成单数或复数选择器。自动订阅与重渲染组件会被自动订阅到该 Fragment 数据的变化上。如果应用任何地方更新了这个User的数据比如新请求返回、或者一次 mutation 改变了数据组件会自动用最新数据重新渲染。这个机制在 useFragmentInternal_CURRENT.js 的subscribeToSnapshot中实现它对environment.lookup(fragmentSelector)得到的Snapshot调用environment.subscribestore 一旦更新就触发setState完成重渲染同时通过handleMissedUpdates处理渲染与订阅建立之间错过的并发 store 更新。自动生成的 Flow 类型运行编译器后Relay 会为每个 Fragment 自动生成 Flow 类型用于声明组件的 props。生成的类型文件名为fragment_name.graphql.js其中包含$key后缀类型如UserComponent_user$keyFragment 引用的类型用来声明 props$data后缀类型如UserComponent_user$data数据形状类型。在本例中data的类型会被推断为{ name: ?string, profile_picture: ?{ uri: ?string } }。类型生成的实现位于 relay-typegen/src/write.rs编译器为每个 Fragment 写出fragment$data数据类型以及以fragment$key命名的引用类型包含$fragmentSpreads与可选的$data字段。你可以在测试夹具 aliased-fragment-raw-response-type.expected 中看到真实的生成产物形态declare export opaque type MyUserFragment$fragmentType: FragmentType; export type MyUserFragment$data { readonly name: ?string, readonly $fragmentType: MyUserFragment$fragmentType, }; export type MyUserFragment$key { readonly $data?: MyUserFragment$data, readonly $fragmentSpreads: MyUserFragment$fragmentType, ... };lint 规则强制类型安全官方推荐使用 eslint-plugin-relay 的 lint 规则强制在使用useFragment时正确声明 Fragment 引用 prop 的类型。只要传入正确类型的 Fragment 引用data的返回类型就会被自动 Flow 类型化无需手动写注解。在同一个组件中渲染多个 Fragment如果需要在一个组件里渲染多个 Fragment 的数据可以多次调用useFragmentimport type {UserComponent_user$key} from UserComponent_user.graphql; import type {UserComponent_viewer$key} from UserComponent_viewer.graphql; const React require(React); const {graphql, useFragment} require(react-relay); type Props { user: UserComponent_user$key, viewer: UserComponent_viewer$key, }; function UserComponent(props: Props) { const userData useFragment( graphql fragment UserComponent_user on User { name profile_picture(scale: 2) { uri } } , props.user, ); const viewerData useFragment( graphql fragment UserComponent_viewer on Viewer { actor { name } } , props.viewer, ); return ( h1{userData.name}/h1 div img src{userData.profile_picture?.uri} / Acting as: {viewerData.actor?.name ?? Unknown} /div / ); } module.exports UserComponent;两次useFragment调用彼此独立userData来自User类型上的 FragmentviewerData来自Viewer类型上的 Fragment各自的订阅、更新与类型推导互不干扰。从实现上看每次调用都会建立自己独立的 selector 和 store 订阅。组合 Fragment组件的局部推理与数据遮罩GraphQL 的 Fragment 是可复用单元因此它可以包含其他 Fragment也就可以被包含进其他 Fragment 或查询中fragment UserFragment on User { name age profile_picture(scale: 2) { uri } ...AnotherUserFragment } fragment AnotherUserFragment on User { username ...FooUserFragment }在 Relay 中你可以用组件组合 Fragment 组合两种方式叠加来实现同样的效果。每个 React 组件都负责获取其直接子组件的数据依赖——正如它必须知道子组件的 props 才能正确渲染它们。这一模式让开发者能够在局部范围内推理组件这个组件需要什么数据、渲染哪些子组件而 Relay 则在整体层面推导出整个 UI 树的数据依赖全局视图。下面是一个标准的父子 Fragment 组件示例。先是子组件UsernameSection/** * UsernameSection.react.js * * Child Fragment Component */ import type {UsernameSection_user$key} from UsernameSection_user.graphql; const React require(React); const {graphql, useFragment} require(react-relay); type Props { user: UsernameSection_user$key, }; function UsernameSection(props: Props) { const data useFragment( graphql fragment UsernameSection_user on User { username } , props.user, ); return div{data.username ?? Unknown}/div; } module.exports UsernameSection;然后是父组件UserComponent它既渲染UsernameSection又在自己的 Fragment 声明里通过...UsernameSection_user包含子组件的 Fragment/** * UserComponent.react.js * * Parent Fragment Component */ import type {UserComponent_user$key} from UserComponent_user.graphql; const React require(React); const {graphql, useFragment} require(react-relay); const UsernameSection require(./UsernameSection.react); type Props { user: UserComponent_user$key, }; function UserComponent(props: Props) { const user useFragment( graphql fragment UserComponent_user on User { name age profile_picture(scale: 2) { uri } # Include child fragment: ...UsernameSection_user } , props.user, ); return ( h1{user.name}/h1 div img src{user.profile_picture?.uri} / {user.age} {/* Render child component, passing the _fragment reference_: */} UsernameSection user{user} / /div / ); } module.exports UserComponent;这个例子揭示了 Relay Fragment 组合的几个核心事实双向包含UserComponent既渲染UsernameSection组件又在自己的graphql声明中通过...UsernameSection_user展开子组件声明的 Fragment。两者缺一不可——组件树负责渲染层级Fragment 展开负责数据依赖层级。传给子组件的是引用不是数据UsernameSection期望的userprop 是一个 Fragment 引用。这个引用本身不包含子组件声明的任何数据子组件会通过自己的useFragment调用用这个引用去读它自己声明的那部分字段。这意味着父组件不会接收到子组件选中的数据除非父组件显式选择了相同字段同样子组件也不会接收到父组件选择的数据除非它自己选了相同字段。这防止了组件之间哪怕是无意中形成隐式依赖——如果子组件可以读到父组件的数据那么修改一个组件就可能悄悄破坏另一个组件。这种能力被称为 数据遮罩data masking是 Relay 保证组件可局部修改、互不干扰的关键设计。引用的来源链路子组件UsernameSection期望的 Fragment 引用来自读取了包含其 Fragment 的父 Fragment 的结果。在本例中读取包含...UsernameSection_user的 Fragment 所得到的data恰好就是UsernameSection期望的那个 Fragment 引用。换言之useFragment读出的数据同时充当了该 Fragment 内所有子 Fragment 的引用。这条规则在 RelayModernSelector.js 与useFragmentInternal的实现中体现为父 Fragment 的 selector 读取结果中子 Fragment 的 spread 位置会保留$fragmentSpreads引用信息见上文 typegen 夹具中MyQuery$data的$fragmentSpreads: MyUserFragment$fragmentType子组件拿到后据此构造自己的 selector。将 Fragment 组合进查询所有 Fragment 都必须生根Fragment 用于声明组件的数据依赖但它本身无法被单独获取。Fragment 必须直接或间接地被包含在一个查询Query中才能被请求到数据。换句话说渲染时所有 Fragment 必须归属于某个查询或者说它们必须根植rooted在某个查询之下。需要注意同一个 Fragment 可以被多个查询包含但当渲染某个具体 Fragment 组件实例时它必须已经作为某个特定查询请求的一部分被包含进来。将 Fragment 组合进查询的方式与 Fragment 之间的组合完全一致参见上文组合 Fragment一节。下面是一个完整的查询组件 Fragment 组件示例。Fragment 组件UserComponent与之前相同/** * UserComponent.react.js * * Fragment Component */ import type {UserComponent_user$key} from UserComponent_user.graphql; const React require(React); const {graphql, useFragment} require(react-relay); type Props { user: UserComponent_user$key, }; function UserComponent(props: Props) { const data useFragment( graphql..., props.user, ); return (...); } module.exports UserComponent;查询组件App使用usePreloadedQuery渲染查询并把查询结果中的user作为 Fragment 引用传给UserComponent/** * App.react.js * * Query Component */ import type {AppQuery} from AppQuery.graphql; import type {PreloadedQuery} from react-relay; const React require(React); const {graphql, usePreloadedQuery} require(react-relay); const UserComponent require(./UserComponent.react); type Props { appQueryRef: PreloadedQueryAppQuery, } function App({appQueryRef}) { const data usePreloadedQuery( graphql query AppQuery($id: ID!) { user(id: $id) { name # Include child fragment: ...UserComponent_user } } , appQueryRef, ); return ( h1{data.user?.name}/h1 {/* Render child component, passing the fragment reference: */} UserComponent user{data.user} / / ); }这个例子同样有两个要点引用来自查询读取结果UserComponent期望的 Fragment 引用来自读取了包含其 Fragment 的父查询的结果——本例即包含...UserComponent_user的AppQuery。也就是说usePreloadedQuery读出的data同样充当了查询中所有子 Fragment 的引用。这与上文中父 Fragment 读取结果充当子 Fragment 引用的规则完全一致只不过把父 Fragment换成了根查询。Fragment 组件必须是查询的后代如前所述渲染时所有 Fragment 都必须归属于某个查询因此所有 Fragment 组件都必须是某个查询组件的后代。这保证了只要从根查询的usePreloadedQuery读取结果出发你总是能为useFragment提供一个 Fragment 引用。在 usePreloadedQuery.js 的实现中查询数据经由useLazyLoadQueryNode渲染并写入 store随后useFragment通过environment.lookup从 store 读取对应数据——这条查询 → store → Fragment的链路正是Fragment 生根于查询的运行时体现。运行时原理useFragment 内部是如何工作的为了更深入地理解useFragment可以结合源码梳理它的核心调用链以 useFragmentInternal_CURRENT.js 为例构造 selectoruseMemo(() getSelector(fragmentNode, fragmentRef), [fragmentNode, fragmentRef])。getSelector将 Fragment 节点与引用合并为ReaderSelector若引用为null则得到null此时组件进入bailout状态返回null/[]数据。校验引用形态通过invariant检查——plural Fragment声明了relay(plural: true)必须接收数组引用非 plural Fragment 必须接收对象引用同时校验引用非空时 selector 必须存在否则会给出父组件忘记 spread 该 Fragment或条件性获取导致引用缺失等错误提示源码中甚至会建议用alias指令修复条件性 spread 的问题。读取 snapshot 并订阅environment.lookup(fragmentSelector)从 store 读取该选择器对应的数据快照底层由 RelayReader.js 的read完成返回包含data、isMissingData、fieldErrors等信息的Snapshot随后subscribeToSnapshot对 snapshot 建立订阅store 更新即触发重渲染。缺失数据时的 Suspense若snapshot.isMissingData为 true且存在会影响该 Fragment 的进行中操作如 owner 查询、相关 mutation组件会通过throw pendingOperationsResult.promise挂起suspend直到数据就绪——这解释了为什么 Fragment 组件在首次挂载或切换 selector 时可能触发 Suspense 边界。返回数据单数 Fragment 直接返回state.snapshot.data不新分配对象避免无谓重渲染plural Fragment 则useMemo生成snapshots.map(s s.data)的数组。此外useFragmentInternal会根据RelayFeatureFlags.ENABLE_ACTIVITY_COMPATIBILITY在CURRENT与EXPERIMENTAL两个实现之间切换见 useFragmentInternal.jsEXPERIMENTAL 版本额外支持Activity的显隐生命周期订阅管理。仓库中对应的行为验证可见于 useFragment-test.js 等一系列测试文件例如useFragment_nullability-test.js覆盖了引用为空/非空时的返回类型行为。小结围绕 Relay 的 Fragment本指南覆盖了完整的知识闭环声明用graphql标签按module_name_property_name约定声明 Fragment编译器自动生成$key/$data类型与产物文件渲染useFragment(fragment, fragmentRef)返回数据并自动订阅更新多次调用可同时渲染多个 Fragment组合通过...ChildFragment展开实现 Fragment 组合配合组件组合传递 Fragment 引用并依靠数据遮罩保证组件的局部可推理性生根Fragment 无法独立获取必须被查询直接或间接包含所有 Fragment 组件因此都必须是查询组件的后代。这四步共同构成了 Relay组件声明式数据依赖的完整体系开发者只关心我的组件需要什么数据而 Relay 负责把分散在各组件里的 Fragment 汇总到查询中统一获取、归一化存储、并在数据变化时精确驱动对应的组件重渲染。建议继续阅读 thinking-in-relay 深入了解数据遮罩的设计哲学并通过 guided-tour 其他章节如 list-data、refetching、updating-data掌握分页、重取与更新的实战用法。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay Fragments 渲染完全指南从 useFragment 到数据遮蔽与查询组合Relay Fragments 渲染完全指南从 useFragment 到数据遮蔽与查询组合 摘要 本文基于 Relay v15 官方文档 Fragments前端开发工具Relay Fragments 实战指南用 useFragment 与数据遮蔽构建可扩展的组件化数据层Relay Fragments 实战指南用 useFragment 与数据遮蔽构建可扩展的组件化数据层 本篇技术指南以 Relay 官方教程v15.0.0前端开发工具Relay Fragments 渲染实战指南使用 useFragment 声明、组合与渲染数据依赖Relay Fragments 渲染实战指南使用 useFragment 声明、组合与渲染数据依赖 Fragment片段是 Relay 中声明 React前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考