从 2019 OSS Day 看 Relay 开源演进:对象身份、SSR、Mutation 语义与编译器配置化
【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay
一、会议背景与纪要定位
meta/meeting-notes/2019-01-25-oss-sync.md记录了 Relay 团队首次举行的 "OSS Day"(开源工作日)会议纪要。这场会议的核心内容包括:与社区协作者会面、跟进上一轮会议遗留的内部讨论,以及推进 Relay 2.0.0 版本的发布工作。核心团队将大部分时间投入到开源事务上,包括与多位外部贡献者讨论路线图和社区关注的重要问题、对 issue 和 PR 进行分流处理(triaging)以及发布新版本。
从这份纪要中,我们可以提炼出 2019 年上半年 Relay 开源工作的关键议题:更灵活的对象身份识别、服务端渲染支持、mutation 结果重叠问题、降低 Relay 使用门槛、本地状态探索、编译器配置化、以及冻结 JSON 标量的修复。下文将逐项深入剖析,并结合当前仓库的源码实现进行验证。
二、2019 H1 路线图:React Suspense 兼容 API 回归开源
会议首先回顾了 issue #2554 评论区 中讨论的 2019 年 H1 路线图。纪要的核心结论是:新的 React Suspense 兼容 API 将尽快移回开源版本(OSS)。
这一决策的历史背景是:当时 Relay 团队正在推进基于 React Suspense 的下一代数据获取 API(也就是后来的useLazyLoadQuery、usePreloadedQuery等 Relay Hooks)。从当前仓库的代码可以看到,这些 API 已经在 react-relay/relay-hooks 中全面落地,例如useLazyLoadQuery.js、usePreloadedQuery.js、useQueryLoader.js等,并提供了配套的.d.ts类型声明。这印证了当时路线图的最终成果。
三、更灵活的对象身份识别(Object Identity)
3.1 问题背景
Relay 的对象身份规范(Object Identification Spec)要求:实体必须通过Node接口的id字段来唯一标识。具体规范可参考 website/spec/ObjectIdentification.md。社区成员 Justin(来自 Artsy)提出,理想情况下,应用即使不完全遵循 Relay 的对象身份规范,也应该能够使用 Relay。主要存在两类不遵循规范的应用:
- 使用全局唯一标识符,但字段名不是
id(例如使用__id); - 完全不使用全局唯一标识符(可能使用类型特定的主键)。
针对第一种情况,PR #2249 提出了一个解决方案:从 schema 的Node接口推断全局 id 字段的名称。例如,如果 schema 的Node接口将 id 字段命名为__id,Relay 就应该使用__id字段作为标识符。
3.2 核心团队的顾虑
核心团队对引入灵活性持谨慎态度,主要顾虑是:它可能给那些确实遵循规范的应用带来运行时开销。使用固定名称查找标识符字段,比额外做一次查找或调用一个函数更高效。
但这也意味着 Artsy 需要维护一个 Relay 的分支,将查找id改为查找__id。更普遍地说,理想情况是支持那些不遵循身份规范的应用。另一个使用场景是:即使应用大部分遵循规范,也可能存在个别不遵循的类型(例如某些类型的id字段并非全局唯一)。
3.3 提出的解决方案选项
会议讨论了两个主要方向:
- 选项一:分叉(Fork)身份解析逻辑。通过模块 shimming、自定义构建等方式,为 Facebook 内部和 OSS 提供不同的身份解析逻辑。OSS 版本调用用户提供的函数来解析对象身份(默认行为与当前一致)。OSS 用户通常更能容忍小的性能折中来换取灵活性,因此这个方案是合理的。
- 选项二:为
LinkedField增加变体。该变体要么指定 id 字段的名称,要么指示在运行时调用某个函数。符合 Relay 对象身份规范的类型继续使用标准的LinkedField(低开销),只有需要定制的类型才承担额外开销。这个方案更灵活,因为它既保留了现有场景的性能,又启用了更多灵活性。
下一步行动:@josephsavona跟进该 PR。
3.4 源码验证:最终落地为可配置的getDataID
从当前仓库源码来看,这一议题的最终解决方案落在了可配置的getDataID函数上(相当于选项一的演进版本)。核心实现在:
- packages/relay-runtime/store/defaultGetDataID.js 定义了默认行为:对普通类型直接返回
fieldValue.id,对viewer类型在id缺失时返回固定的VIEWER_ID; - packages/relay-runtime/store/RelayStoreTypes.js 中定义了
GetDataID的类型签名(fieldValue: {...}, typeName: string) => DataID; - packages/relay-runtime/store/NormalizationEngine.js 在规范化引擎中使用
config.getDataID ?? defaultGetDataID,即用户不配置时自动回退到默认实现; - packages/relay-runtime/store/OperationExecutor.js 通过构造参数注入
getDataID,并沿整个执行链路(第 791、910、995、1462 行)传递。
也就是说,现代 Relay 允许应用通过环境配置覆盖getDataID,自定义对象身份的解析逻辑——这正是当年讨论中"OSS 版本调用用户提供的函数来解析对象身份"的直接产物。同时,defaultGetDataID作为默认实现保证了不配置时的低开销,兼顾了讨论中提到的性能诉求。
四、服务端渲染(Server-side Rendering)经验与挑战
社区成员 Rob Richard 分享了用 Relay 实现服务端渲染的经验,并提出了几个遇到的问题。
4.1 实践路线
Rob 发现更灵活的做法是不直接使用QueryRenderer,而是:
- 手动获取数据;
- 设置 context;
- 渲染;
- 将数据序列化到客户端用于初始化(seed)环境;
- 在客户端同样手动设置 context 并渲染。
这种方式的限制是:服务端不会预取嵌套的QueryRenderer,不过由于 fragment 组合(fragment composition)的特性,初始加载时通常也不需要嵌套渲染器。
4.2 具体问题与进展
- 内联 require(inline requires)性能问题:Relay 使用内联 require 在 Node.js 上较慢;纪要说截至撰写时,OSS 构建已禁用内联 require。
- React context 不稳定 API:Relay 当时使用了一个不稳定的 API 读取 React context 且不工作,已修复为使用另一个当前可用的不稳定 API。Relay 团队会在有最终 API 后再更新。
@defer支持:对@defer的内置支持将解决嵌套查询渲染器的情况。Rob 正在使用 Apollo server 实验性的@defer,配合自定义 network layer 来修补增量 payload。requestCache相关 bug:QueryRenderer的共享requestCache存在相关问题。下一步行动:@jstejada负责排查。
4.3 源码验证:requestCache的实现
从当前仓库源码看,requestCache是 packages/relay-runtime/query/fetchQueryInternal.js 中按环境维护的请求缓存:
- 第 37 行通过
WEAKMAP_SUPPORTED判断是否用 WeakMap 为每个环境建立requestCachesByEnvironment; - 第 127-160 行展示了典型的缓存读写流程:先
getRequestCache(environment)获取缓存,再按RequestIdentifier查缓存,命中则复用,未命中则发起请求并在.finally()中清理; - 第 182-210 行展示了
RequestCacheEntry的复用逻辑(getObservableForCachedRequest)。
这段源码印证了纪要中"共享 requestCache"的设计——多个QueryRenderer共享同一环境内的请求缓存以去重,而当时正是这个共享缓存的行为存在 bug,需要跟进修复。
4.4 后续演进
纪要提到:"即将推出的新 API / 变更将使使用 QueryRenderer 从缓存中的数据同步渲染、必要时手动设置 context 变得更加容易。"结合 2019 H1 路线图(Suspense 兼容 API 回归 OSS),这正是后来 Relay Hooks 体系中usePreloadedQuery、loadQuery等 API 提供的服务端渲染能力。当前仓库 packages/react-relay/relay-hooks 与 packages/relay-runtime/query/fetchQuery.js 已经承载了这套能力。
五、Mutation 结果重叠(Mutation Overlap)问题
5.1 问题与官方推荐 workaround
Rob Richard 指出:不同的 mutation 结果在 store 中可能发生重叠。官方推荐的 workaround 是:在 mutation 变量中包含一个用于破坏缓存的(caching-breaking)值,例如将clientMutationId参数设置为自增的值。
这一问题的本质是:如果两个 mutation 写入的数据在 store 中共享了相同的记录和路径,后一个 mutation 的响应可能覆盖前一个的结果,或者触发意外的订阅通知。
5.2 解决 PR 与讨论结论
针对此问题的 PR #2349 提出:为每个 mutation 设置一个新的唯一 root id。核心团队对这一方向表示支持。下一步行动:@josephsavona跟进该 PR。
从设计哲学上看,这个方案与"为每个请求生成唯一标识"的思路一脉相承。当前仓库中,packages/relay-runtime/store/RelayModernOperationDescriptor.js 负责生成 operation descriptor,包含request、fragment、root等标识;而clientMutationId作为缓存破坏变量的实践,在今天依然是 Relay 处理并发 mutation 的常用手段。
六、让 Relay 更容易上手
Sibelius 反馈:经常听到"Relay 比 Apollo 难用"的说法。会议深入拆解了"到底哪里难"。
6.1 实际难点分析
- 认知误区:Apollo 和 Relay 都支持类似的模式——声明一个包含所有字段的单一 query(Apollo 风格)vs 将 fragment 与组件放在一起(colocation)。但人们往往没意识到:使用 Relay 时可以先用简单 query,等应用规模增长后再逐步引入 colocated fragments。
- 价值传递不足:人们常常不理解 colocation 和数据掩蔽(data-masking)的价值。刚起步时这不是问题,但随着应用扩展,其重要性会凸显。
- 文档与示例:文档和示例可以帮助阐明:fragment / colocation / masking 是高级概念,一开始不必使用。
6.2 其他难点
- 编译器需要配置:相关的可配置化 PR 在后面讨论(见第八节)。
- 更好的文档:已在路线图规划中。
- 示例:欢迎社区帮助。
- 能否使用 babel-macros 运行编译器?这是 create-react-app 支持 Relay 的方式。
- 对新人的整体门槛:如果你是不用 GraphQL 的 React 开发者,需要配置的东西很多(schema、server 库、client 库等)。
下一步行动:Relay 团队思考如何在官网/文档中把价值主张(value proposition)和目标受众(intended audience)表达得更清晰。
6.3 源码验证:文档与上手路径的现状
从当前仓库看,这一议题的后续成果体现在多个方面:
- website/docs/getting-started 提供了一套分步入门教程;
- website/docs/guided-tour 将 fragment、colocation、data-masking 等概念组织成专题向导;
- packages/babel-plugin-relay 中保留了 Babel 插件形态的编译入口(
BabelPluginRelay.js、compileGraphQLTag.js),这正是当时讨论的 babel-macros / babel 插件思路在开源版中的延续(create-react-app 通过它支持 Relay)。
七、本地状态(Local State)的探索
Sibelius 询问 Relay 对本地状态(local state)的规划。核心团队回应:正在与 React 团队及其他同事协作探索,目前还没有具体方案,但在思考 React Suspense 和 Concurrent mode 背景下,复杂本地状态应该如何工作。
从当前仓库看,这一探索最终演化为:
- 客户端 schema 扩展(client schema extensions)与客户端字段(client fields),见 packages/relay-test-utils-internal/schema-extensions;
- 客户端 query / 本地 query 支持,见 react-relay/relay-hooks/useClientQuery.js;
commitLocalUpdate等本地更新 API,见 packages/relay-runtime/mutations/commitLocalUpdate.js。
八、Relay 编译器可配置化(Compiler Options)
issue #2518 提出了一种思路:像 Babel、Metro 等 JS 工具一样,通过配置文件来配置编译器。讨论结论:Relay 团队表示支持,cosmiconfig库看起来不错,社区若能提供详细方案文档将不胜感激(见 issue 评论区)。
8.1 源码验证:relay.config.json的落地
这一议题在开源版的最终成果,就是今天每个 Relay 项目根目录下的relay.config.json。当前仓库中的相关证据:
- compiler/crates/relay-config 是完整的配置解析 crate,包含
src/下多个模块(如connection_interface.rs、defer_stream_interface.rs、js_module_format.rs等),通过schemars生成 JSON Schema; - compiler/crates/relay-compiler/relay-compiler-config-schema.json 是编译器配置的 JSON Schema 定义,供编辑器与工具链校验配置;
- compiler/test-project/relay.config.json 是真实的配置示例;
- 编译器默认会在项目根目录自动查找
relay.config.json,支持src、schema、language、artifactDirectory等关键配置项。
当年讨论的"像 Babel、Metro 一样通过配置文件配置编译器"的设想,如今已通过relay.config.json成为 Relay 编译器(compiler/crates/relay-compiler)的标准工作方式。
九、冻结 JSON 标量修复(Frozen JSON Scalars)
9.1 问题背景
Rob 提到 PR #2193 处于开放状态。这个问题影响一些使用复杂标量(complex scalars,如 JSON 标量)的 OSS 用户。
9.2 修复方案与讨论
- 修复方式:在
recycleNodesInto中停止在 DEV 模式下修改被冻结(frozen)的对象。 - 顾虑:这会在技术上造成 DEV 与生产模式之间的可观察行为差异(observable behavior change),而历史上团队一直尽量避免这种差异。
- 权衡:但这只发生在自定义标量数据上,不涉及标准 Relay 数据。
- 结论:决定推进该修复以解除社区阻碍,因为复杂标量本身就是 opt-in 的。另外需要提醒:如果使用复杂标量,其对象身份的稳定性并不能得到保证。该 PR 已合入。
9.3 源码验证:recycleNodesInto的冻结保护
当前仓库中,packages/relay-runtime/util/recycleNodesInto.js 的注释明确写道:"Recycles subtrees fromprevDataby replacing equal subtrees innextData.Does not mutate a frozen subtree."(回收prevData中相等的子树以替换到nextData,不会修改被冻结的子树)。
具体实现细节:
- 第 44 行与第 64 行分别对数组和对象计算
canMutateNext = canMutate && !Object.isFrozen(nextArray / nextObject),即先检查目标是否被冻结,冻结则放弃原地写入; - 第 53-55 行、73-76 行只有在
canMutateNext为 true 时才写回nextArray[ii]/nextObject[key]; - packages/relay-runtime/util/shallowFreeze.js 的注释同样说明其用途:"Shallow freeze to prevent Relay from mutating the value in recycleNodesInto or deepFreezing the value"(浅冻结以防止 Relay 在
recycleNodesInto中修改该值)。
这正是 2019 年 1 月那次讨论的修复在今日源码中的最终形态:在回收子树时尊重冻结状态,避免 DEV 模式下修改用户提供的自定义标量对象。
十、从纪要看 Relay 的开源协作模式
这份纪要在方法论层面也很有价值,它体现了 Relay 团队当时建立的开源协作节奏:
- 定期 OSS Day:核心团队集中一整天处理开源事务,包括与社区贡献者会面;
- 社区→内部的双向通道:外部贡献者(如 Artsy 的 Justin、Rob Richard、Sibelius)提出真实生产环境中的痛点,核心团队评估取舍后给出方向;
- 明确的下一步行动:每个议题都标注了负责人(如
@josephsavona、@jstejada),确保讨论不落空; - 性能与灵活性的权衡:反复出现的主题是"灵活性的代价",无论是对象身份(额外查找 vs 固定字段)、冻结保护(DEV/生产行为差异),还是编译器配置(配置解析开销)。
这些议题与当前仓库源码的对应关系,可以概括为下表:
| 纪要议题 | 当前仓库中的落地点 | 关键证据 |
|---|---|---|
| 对象身份灵活性 | 可配置getDataID | defaultGetDataID.js、NormalizationEngine.js |
| 服务端渲染 | requestCache按环境缓存、Relay Hooks | fetchQueryInternal.js、relay-hooks |
| Mutation 重叠 | operation descriptor /clientMutationId实践 | RelayModernOperationDescriptor.js |
| 降低上手门槛 | 分步入门文档、Babel 插件编译 | website/docs/getting-started、packages/babel-plugin-relay |
| 本地状态 | client schema / client query /commitLocalUpdate | useClientQuery.js、commitLocalUpdate.js |
| 编译器配置化 | relay.config.json+ JSON Schema | relay-config、relay-compiler-config-schema.json |
| 冻结 JSON 标量 | recycleNodesInto尊重冻结状态 | recycleNodesInto.js、shallowFreeze.js |
结语
2019-01-25-oss-sync.md这份纪要虽然只有一页,却浓缩了 Relay 从"Facebook 内部框架"走向"成熟开源框架"的关键转折期决策。会议中讨论的每一个议题——对象身份、SSR、mutation 语义、开发者体验、编译器配置、复杂标量——几乎都能在当前仓库的源码中找到直接或间接的落点。对于研究 Relay 演进历史、理解其设计取舍(尤其是"性能 vs 灵活性"这一主线)的读者来说,这份纪要与仓库源码对照阅读,是理解 Relay 架构决策的最佳路径。
【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考