从 2019 OSS Day 看 Relay 开源演进:对象身份、SSR、Mutation 语义与编译器配置化
2026/9/20 23:36:07 网站建设 项目流程

从 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(也就是后来的useLazyLoadQueryusePreloadedQuery等 Relay Hooks)。从当前仓库的代码可以看到,这些 API 已经在 react-relay/relay-hooks 中全面落地,例如useLazyLoadQuery.jsusePreloadedQuery.jsuseQueryLoader.js等,并提供了配套的.d.ts类型声明。这印证了当时路线图的最终成果。

三、更灵活的对象身份识别(Object Identity)

3.1 问题背景

Relay 的对象身份规范(Object Identification Spec)要求:实体必须通过Node接口的id字段来唯一标识。具体规范可参考 website/spec/ObjectIdentification.md。社区成员 Justin(来自 Artsy)提出,理想情况下,应用即使不完全遵循 Relay 的对象身份规范,也应该能够使用 Relay。主要存在两类不遵循规范的应用:

  1. 使用全局唯一标识符,但字段名不是id(例如使用__id);
  2. 完全不使用全局唯一标识符(可能使用类型特定的主键)。

针对第一种情况,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,而是:

  1. 手动获取数据;
  2. 设置 context;
  3. 渲染;
  4. 将数据序列化到客户端用于初始化(seed)环境;
  5. 在客户端同样手动设置 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相关 bugQueryRenderer的共享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 体系中usePreloadedQueryloadQuery等 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,包含requestfragmentroot等标识;而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.jscompileGraphQLTag.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.rsdefer_stream_interface.rsjs_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,支持srcschemalanguageartifactDirectory等关键配置项。

当年讨论的"像 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 团队当时建立的开源协作节奏:

  1. 定期 OSS Day:核心团队集中一整天处理开源事务,包括与社区贡献者会面;
  2. 社区→内部的双向通道:外部贡献者(如 Artsy 的 Justin、Rob Richard、Sibelius)提出真实生产环境中的痛点,核心团队评估取舍后给出方向;
  3. 明确的下一步行动:每个议题都标注了负责人(如@josephsavona@jstejada),确保讨论不落空;
  4. 性能与灵活性的权衡:反复出现的主题是"灵活性的代价",无论是对象身份(额外查找 vs 固定字段)、冻结保护(DEV/生产行为差异),还是编译器配置(配置解析开销)。

这些议题与当前仓库源码的对应关系,可以概括为下表:

纪要议题当前仓库中的落地点关键证据
对象身份灵活性可配置getDataIDdefaultGetDataID.js、NormalizationEngine.js
服务端渲染requestCache按环境缓存、Relay HooksfetchQueryInternal.js、relay-hooks
Mutation 重叠operation descriptor /clientMutationId实践RelayModernOperationDescriptor.js
降低上手门槛分步入门文档、Babel 插件编译website/docs/getting-started、packages/babel-plugin-relay
本地状态client schema / client query /commitLocalUpdateuseClientQuery.js、commitLocalUpdate.js
编译器配置化relay.config.json+ JSON Schemarelay-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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询