Relay 数据获取哲学:从 Thinking in Relay 看声明式组件化数据依赖
【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay
Relay 将 React 的组件化与声明式思想延伸到了数据获取层:组件只声明"我需要什么数据",而由框架静态地、自动地把整棵组件子树的数据需求合并成一次网络请求。本文基于仓库中 thinking-in-relay.md 的核心脉络,结合源码与官方 Guided Tour 文档,带你掌握 Fragment、Query、useFragment、useLazyLoadQuery与数据掩蔽(Data Masking)这套完整实践,理解为什么 Relay 是"声明式数据获取"的框架而非普通 GraphQL 客户端。
从 React 到 Relay:把声明式思想迁移到数据层
Relay 的数据获取方式深受 React 经验启发。React 将复杂的界面拆解为可复用的组件(components),让开发者能够隔离地思考应用的离散单元,同时降低应用不同部分之间的耦合;更重要的是,这些组件是**声明式(declarative)**的——开发者只需描述"给定某个状态,UI 应该长什么样",而无需关心"如何"去改变 UI。不同于以往用命令式指令操作原生视图(如 DOM)的做法,React 通过一份 UI 描述自动推导出所需执行的命令。
Relay 把同样的思想带到了数据层:组件声明自己需要的数据,Relay 负责推导并执行获取数据的命令。这正是理解整个框架的起点,后续的 Fragment、Query、Data Masking 等概念都是这一思想的直接产物。
为视图获取数据:两种朴素方案各自的缺陷
绝大多数产品的需求非常一致:在显示 loading 指示器的同时,获取一个视图层级所需的全部数据,待数据就绪后一次性渲染整个视图。围绕"如何获取视图数据",有两种看似合理的朴素方案,但都存在明显问题:
- 方案一:由根组件统一声明并获取自己和所有子组件的数据。这引入了强耦合:任何子组件的数据需求发生变化,都必须去修改所有可能渲染它的根组件。耦合意味着更高的出 Bug 概率,也会拖慢开发节奏。
- 方案二:每个组件各自声明并获取自己所需的数据。听起来很理想,但问题在于:组件可能根据它收到的数据去渲染不同的子组件,于是嵌套组件只有在父组件的查询完成之后才能开始渲染和获取数据。也就是说,数据获取被迫分阶段串行进行:先渲染根并获取它的数据,再渲染子组件并获取它们的数据,一直推进到叶子组件——渲染过程将产生多次慢速、串行的网络往返(waterfall)。
Relay 把两种方案的优势结合起来:允许组件各自指定数据需求,但把这些需求静态合并(coalesce)成一个查询,一次性获取整棵组件子树的数据。关键在于,Relay 是在代码编写时(应用运行之前)静态地确定整个视图的数据需求的,而不是在运行时动态拼接。这一能力建立在 GraphQL 之上:函数组件用一个或多个 GraphQL Fragment 描述数据需求,Fragment 彼此嵌套,最终嵌套进 Query;当该 Query 被获取时,Relay 会为它及其所有嵌套 Fragment 发出单次网络请求。
用 Fragment 声明组件的数据需求
在 Relay 中,组件的数据需求通过Fragment指定。Fragment 是一段有名字的 GraphQL 片段,声明从某个特定类型的对象上选择哪些字段,使用graphql字面量书写。例如下面的AuthorDetails_author片段声明了要获取作者的名字和照片 URL:
// AuthorDetails.react.js const authorDetailsFragment = graphql` fragment AuthorDetails_author on Author { name photo { url } } `;随后在函数组件中调用useFragment(...)钩子从 store 中读出这些数据。真正"从哪个作者身上读"由传给useFragment的第二个参数决定:
// AuthorDetails.react.js export default function AuthorDetails(props) { const data = useFragment(authorDetailsFragment, props.author); // ... }关于useFragment,有几个关键点值得展开:
- Fragment Reference(片段引用):第二个参数(
props.author)是一个"片段引用"。它不是数据本身,而是一个指向"特定类型实例"的指针——Relay 用它去 store 中读取该片段声明的字段。引用通过把 Fragmentspread进另一个 Fragment 或 Query 得到。 - Fragment 不能单独被获取:所有 Fragment 最终都必须(直接或传递地)spread 进某个 Query,数据才会真正被拉取。这一点在 fragments.md 中被反复强调:"Fragments can't be fetched by themselves"。
- 自动订阅更新:使用
useFragment渲染的组件会自动订阅该 Fragment 数据的更新,当数据在应用任何位置被修改(如新数据到达、mutation 提交),组件会自动使用最新数据重新渲染。 - 命名约定:Fragment 名需要全局唯一,官方约定按"模块名 + 属性名"命名:
<module_name>_<property_name>,例如AuthorDetails_author。这样既便于定位 Fragment 定义在哪个模块,也避免同模块内多个 Fragment 的命名冲突。 - 生成类型:Relay 编译器为每个 Fragment 自动生成 Flow/TS 类型,其中带
$key后缀的类型表示片段引用(如AuthorDetails_author$key),带$data后缀的类型表示数据形状,可从编译产物<fragment_name>.graphql.js中导入。
从源码看,useFragment的实现路径是:先通过getFragment把graphql标签节点解析为FragmentNode,再委托给useFragmentInternal完成实际的读取与订阅逻辑,并依据特性开关在useFragmentInternal_CURRENT(当前实现)与useFragmentInternal_EXPERIMENTAL(实验实现)之间切换,参见 packages/react-relay/relay-hooks/useFragment.js 与 packages/react-relay/relay-hooks/useFragmentInternal.js。在开发模式下,它还会通过useDebugValue暴露当前渲染的 fragment 名称与数据,便于调试。
用 Query 聚合 Fragment:一次请求渲染整个视图
只有 Fragment 还不够——它们必须被聚合进 Query 才能真正获取数据。假设我们要渲染一篇 Story 的标题和作者详情,可以声明一个 spread 了AuthorDetails_author的查询:
// Story.react.js const storyQuery = graphql` query StoryQuery($storyID: ID!) { story(id: $storyID) { title author { ...AuthorDetails_author } } } `;然后用useLazyLoadQuery获取并渲染它:
// Story.react.js function Story(props) { const data = useLazyLoadQuery(storyQuery, props.storyId); return (<> <Heading>{data?.story.title}</Heading> {data?.story?.author && <AuthorDetails author={data.story.author} />} </>); }注意这里发生了什么:我们发出的单次网络请求同时包含了Story组件和AuthorDetails组件所需的数据;数据就绪后,整个视图可以一次性渲染完成,无需任何串行往返。data.story.author(若存在;默认所有字段都可空)就是可以传给AuthorDetails的 fragment reference——useLazyLoadQuery返回的查询结果天然充当其内部所有子 fragment 的引用来源。
关于查询类 API,queries.md 给出了完整的三种模式,可以按场景选用:
useLazyLoadQuery(query, variables):组件渲染时才(惰性)发起获取,是起步最简单的 API。但文档明确提示:不加节制地使用惰性加载容易引发嵌套式/瀑布式往返,降低性能。usePreloadedQuery(queryRef, ...)+loadQuery/useQueryLoader:推荐的"render-as-you-fetch"模式——在组件渲染之前(如路由切换的事件处理器中、应用初始化阶段)就发起获取,拿到一个PreloadedQuery引用,再传给渲染组件。这能把请求提前到渲染之前,尽早展示内容,也便于配合 React Suspense 设计加载态。注意loadQuery在 React 渲染阶段调用会直接抛错。- Suspense 集成:查询组件是"可挂起"的组件,配合
<Suspense fallback={...}>即可声明式地展示 loading 占位 UI,避免闪烁,参见 loading-states.md。
useLazyLoadQuery的签名与选项在源码中有完整注释(packages/react-relay/relay-hooks/useLazyLoadQuery.js),其中fetchPolicy控制缓存与网络请求的组合策略,默认值为"store-or-network"(优先复用本地缓存,仅当数据缺失时才发请求),其余可选值包括:
| 取值 | 行为 |
|---|---|
store-or-network | (默认)复用本地缓存,仅在数据缺失时发网络请求;完全命中缓存则不发请求 |
store-and-network | 复用本地缓存,但无论缓存是否完整都总是发网络请求 |
network-only | 忽略本地缓存,总是发网络请求 |
store-only | 只用本地缓存,永不发网络请求,适合读取纯本地数据 |
此外fetchKey可强制组件在重新渲染时重新求值查询与变量,networkCacheConfig默认{force: true}用于绕过网络层的额外响应缓存。
数据掩蔽(Data Masking):消灭隐式依赖
在典型的数据获取方式中,两个组件之间经常存在隐式依赖(implicit dependencies)。例如<Story />可能使用了某份数据,却并没有直接确保它被获取——这份数据恰好由系统其他部分(比如<AuthorDetails />)拉取。一旦修改<AuthorDetails />并删除了那段数据获取逻辑,<Story />就会毫无预兆地坏掉。这类 Bug 往往不会立刻显现,尤其在大团队维护的大型应用中;手工测试与自动化测试能帮上的忙有限——这正是适合交给框架来解决的系统性问题。
Relay 为此提供了两个层次的保障:
- 最小可见性(data masking):组件只能访问自己在 GraphQL Fragment 中明确请求的字段,除此之外什么都看不到。查询了 Story
title的组件看不到别人查询的text;更严格的是,父组件连子组件请求的数据也看不到——否则同样会破坏封装。 - 不透明引用的校验:Relay 使用 props 上的不透明标识(fragment reference)来验证"渲染组件前已显式获取其数据"。如果
<Story />渲染<AuthorDetails />却忘了 spread 它的 fragment,Relay 会告警数据缺失;哪怕别的组件碰巧获取了相同的数据,Relay 依然会告警——这告诉我们:现在也许能跑,但将来极可能坏掉。
fragments.md 对组合场景的剖析很透彻:父组件UserComponent同时渲染并 spread 子组件UsernameSection的 fragment;传给子组件的 fragment reference本身不携带子组件声明的任何数据,子组件内部用useFragment自行读取。正因如此,任何组件都无法——哪怕是"意外地"——对其他组件产生隐式依赖,开发者可以放心地局部修改组件而不用担心波及他人。
背后的机制:归一化缓存与运行时支撑
"一次请求拿全部数据"与"组件各自声明数据"看似矛盾,之所以能同时成立,是因为 Relay 的运行时架构(详见 runtime-architecture.md 与 architecture-overview.md):
- 归一化对象图缓存:Relay Runtime 维护一个以
DataID为键、以Record为值的RecordSource作为缓存。层级化的 GraphQL 响应被"展平"成记录,每个服务器实体无论被多少个查询获取,都只存储一份;__ref链接用于表达记录间的引用(包括循环图)。 - 编译器与运行时解耦:Relay 编译器负责把
graphql字面量编译为运行时消费的标准产物,运行时则提供归一化缓存、优化后的 write/read 操作、垃圾回收、乐观更新与订阅等能力;React/Relay 只是构建在运行时之上的高层产品 API(如useFragment)。 - 视图一致性:Relay 维护"每个 UI 视图 → 它引用的 ID 集合"的映射;写入缓存时,只通知订阅了受影响 ID 的视图重新渲染,未受影响的视图跳过重渲染——这与 Data Masking 共同构成了"局部推理、全局一致"的保证。
也就是说,Think in Relay 所讲的"静态合并数据需求、单次请求、数据掩蔽",最终都落地在编译器与归一化运行时这两个坚实的基础上,而不是魔法。
总结
GraphQL 为构建高效、解耦的客户端应用提供了强大工具,Relay 则在其上构建起一套声明式数据获取的框架:通过把"取什么数据"(what)与"怎么取"(how)分离,组件各自用 Fragment 声明需求,框架静态合并为单次查询、用数据掩蔽保证封装、用归一化缓存保证一致性,让应用在默认情况下就健壮、透明且高性能。React、Relay、GraphQL 单独来看都很强大,而它们的组合正是一个能让你快速前进、规模化交付高质量应用的 UI 平台。
延伸阅读
- Thinking in GraphQL:REST 到归一化缓存的演进
- Guided Tour:Fragment 的声明、组合与渲染
- Guided Tour:Query 的三种获取模式与 render-as-you-fetch
- Guided Tour:用 Suspense 渲染加载态
- 运行时架构:归一化缓存与 Store 操作
- 架构总览:编译器、运行时与 React/Relay 三层
【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考