Relay 缺失数据处理指南使用 Missing Field Handlers 复用缓存数据【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay导读在 Relay 应用中Store 的缓存复用是提升渲染性能的关键。但当两个不同的 GraphQL 查询指向同一份数据时例如user(id: 4)与node(id: 4)Relay 默认无法识别它们之间的等价关系导致本可复用的缓存被判定为数据缺失。本文基于 Relay 17 官方指南深入讲解missingFieldHandlers的配置方法、三种 handler 类型scalar、linked、pluralLinked的语义并结合 DataChecker.js 的源码级实现说明缺失字段探测与替换的完整流程。读完本文你将能精确编写自己的缺失字段处理器让 Relay 在命中缓存时认得更多等价查询。为什么需要 Missing Field Handlers在开始编写处理器之前先明确它要解决的问题。Relay 天然支持完全相同的查询的缓存复用如果同一个 query 被请求两次第二次执行时 Relay 能直接从 Store 中命中全部数据。但实际业务中不同的查询往往以不同的入口访问同一份数据。典型的例子// Query 1 query UserQuery { user(id: 4) { name } } // Query 2 query NodeQuery { node(id: 4) { ... on User { name } } }这两条查询的文本不同但指向的是同一份数据ID 均为 4 的 User 记录。理想情况下只要其中一条查询已缓存渲染另一条时就应该直接复用。问题在于Relay 默认并不知道node(id: 4)与user(id: 4)是同一份数据它缺少这种领域知识。missingFieldHandlers正是用来弥补这一知识缺口的机制通过为 RelayEnvironment 提供自定义处理器告诉它当某个根字段缺失时可以用另一个 ID 去 Store 中查找对应记录。适用场景从官方指南与实现来看这种机制特别适合Node 接口模式GraphQL 服务实现了node(id:)中继规范同时业务查询又通过具体类型user、story的根字段访问同一对象不同根字段别名同一实体例如user(id:)与profile(user_id:)指向同一用户跨字段 ID 复用如story(story_id:)与node(id:)指向同一 Story。配置入口把 Handlers 交给 RelayEnvironment在 RelayModernEnvironment.d.ts 中Environment 的配置类型明确声明了该选项readonly missingFieldHandlers?: readonly MissingFieldHandler[] | null | undefined;即missingFieldHandlers是MissingFieldHandler的可选数组。构造 Environment 时直接传入即可const {ROOT_TYPE, Environment} require(relay-runtime); const missingFieldHandlers [ { handle(field, record, argValues) { // Make sure to add a handler for the node field if ( record ! null record.getType() ROOT_TYPE field.name node argValues.hasOwnProperty(id) ) { return argValues.id; } if ( record ! null record.getType() ROOT_TYPE field.name user argValues.hasOwnProperty(id) ) { // If field is user(id: $id), look up the record by the value of $id return argValues.id; } if ( record ! null record.getType() ROOT_TYPE field.name story argValues.hasOwnProperty(story_id) ) { // If field is story(story_id: $story_id), look up the record by the // value of $story_id. return argValues.story_id; } return undefined; }, kind: linked, }, ]; const environment new Environment({/*...*/, missingFieldHandlers});几点关键说明ROOT_TYPE从relay-runtime中导入代表查询根节点Query Root的类型标识定义见 RelayStoreUtils.d.ts。判断record.getType() ROOT_TYPE是为了确保该处理器只对根字段生效处理器对node(id:)、user(id:)、story(story_id:)三种根字段分别返回对应的参数值作为目标记录 ID当字段不匹配任何分支时返回undefined表示本处理器不处理该字段Relay 会继续尝试下一个处理器或最终判定数据缺失。Handler 的三种类型与 Handle 函数语义类型定义三种 kind从 RelayStoreTypes.d.ts 中的MissingFieldHandler类型定义可以看到handler 一共支持三种kindkind适用字段handle 返回值用途scalar标量字段数字、字符串等unknown标量值为缺失的标量字段提供替代值linked链接到单个对象的字段DataID \| null \| undefined返回 Store 中另一条记录的 ID代替缺失的链接字段pluralLinked链接到对象数组的字段ArrayDataID \| null \| undefined \| null \| undefined返回一组记录 ID代替缺失的复数链接字段官方指南即本文所依据的 filling-in-missing-data.md重点讲解前两种第三种pluralLinked是类型定义中额外支持的形式适用于friends(first:)这类返回对象列表的字段。handle 函数的四个入参从类型定义看handle函数实际接收四个参数field、parentRecord、args、storefield缺失的字段描述对象NormalizationScalarField/NormalizationLinkedField/ReaderLinkedField可通过field.name读取字段名parentRecord该字段所属的父记录ReadOnlyRecordProxy。在本文示例中即查询根记录因此用record.getType() ROOT_TYPE做判断args当前查询执行时传给该字段的参数Variables通过argValues.hasOwnProperty(id)判断并取值store只读的记录源代理ReadOnlyRecordSourceProxy必要时可进一步读取 Store 中其他记录。返回值语义处理scalar字段时handle应返回一个标量值直接用作缺失字段的值处理linked字段时handle应返回一个ID指向 Store 中另一条应被用于替代该字段的记录返回undefined表示不处理Relay 会继续尝试剩余处理器只有所有匹配的处理器都返回undefined后Relay 才会最终宣布数据缺失。源码级原理缺失探测与替换的完整流程官方指南指出当 Relay 尝试从本地缓存满足查询时只要检测到任何缺失数据它都会在最终宣布数据缺失之前运行所有匹配该字段类型的缺失字段处理器。 这段描述对应的是relay-runtime中的DataChecker数据可用性检查器实现在 DataChecker.js。检查器如何工作文件头部的注释清晰地定义了它的职责DataChecker.js同步检查满足给定selector所需的记录是否都存在于source中。如果字段缺失则使用提供的 handlers 尝试替换数据。target会记录所有因替换成功而被修改的记录。如果所有记录都存在返回true否则返回false。换句话说DataChecker 是读取缓存前先做体检的环节它遍历查询所需的所有字段逐字段确认 Store 中是否有值缺失时依次调用各 handler 尝试补值并把这些替换写入目标记录源。三种缺失字段的处理实现DataChecker 内部为三种缺失情况分别实现了处理函数_handleMissingScalarFieldDataChecker.js遍历所有kind scalar的处理器取第一个返回值非undefined的结果作为缺失字段的值若全部失败则记入缺失日志并标记_recordWasMissing_handleMissingLinkFieldDataChecker.js遍历kind linked的处理器且对返回的 ID 做了额外校验——只有该 ID 在目标记录源中真实存在状态为EXISTENT时才接受这保证了 handler 返回的 ID 不会指向空壳记录_handleMissingPluralLinkFieldDataChecker.js遍历kind pluralLinked的处理器要求返回的 ID 数组全部存在才接受返回null则视为显式表示该字段为 null。三个函数在处理器全部返回undefined后都会通过日志系统发出store.datachecker.missing事件包含kind、dataID、fieldName、storageKey最终调用_handleMissing()将整体结果标记为存在缺失数据从而触发真正的网络请求。从调用链看数据流向在 DataChecker.js 的check函数中handlers 由 Environment 构造时传入并最终注入 DataCheckerDataChecker.js。Environment 层面的接入点包括RelayModernEnvironment.js 的missingFieldHandlers配置属性MultiActorEnvironment.js 与 ActorSpecificEnvironment.js说明多 Actor 环境同样透传该配置RelayPublishQueue.js 在发布队列中也持有一份 handlers用于在数据校验阶段调用。另外值得注意readUpdatableQuery与readUpdatableFragment见 createUpdatableProxy.js同样接受missingFieldHandlers意味着这一机制不只服务于查询渲染也覆盖了可更新代理读取缺失数据时的替换逻辑。编写高质量 Handler 的实践要点综合官方指南与源码实现编写missingFieldHandlers时有以下几点值得注意按kind精确声明处理范围handler 只对与自身kind匹配的缺失字段生效。声明为linked的处理器不会参与标量字段的补值反之亦然。用ROOT_TYPE限定根字段官方示例中所有分支都先判断record ! null record.getType() ROOT_TYPE避免误处理非根字段的同名 field。用hasOwnProperty校验参数存在性直接使用argValues.id前先确认参数确实传入防止对参数缺失的查询做出错误替换。返回undefined表示不处理这是让 Relay 继续尝试下一个处理器、或最终判定数据缺失的正确做法。切勿返回undefined之外的有误导性的值。linked返回的 ID 会被二次校验源码显示DataChecker.js只有返回的 ID 在 Store 中已存在记录状态EXISTENT时替换才会生效——所以 handler 应当确保返回的 ID 对应的数据此前已通过其他查询缓存进 Store。从field.name与参数值联合判断官方示例用field.name node加argValues.hasOwnProperty(id)联合判定这正是字段名 参数双条件匹配的典型写法。与默认行为及后续章节的关系官方文档把本文主题放在reusing-cached-data复用缓存数据章节之下其前序内容讲解的是完全或部分缓存数据的复用本文则处理Relay 无法自动识别的等价数据这一进阶场景。补充说明Relay 也提供了内置的默认缺失字段处理器getDefaultMissingFieldHandlers()见 index.d.ts处理标准 Node 接口场景自定义 handlers 是在此基础上按需扩展即便配置了 handler若替换后仍有字段缺失Relay 仍会判定数据不完整并触发网络请求DataChecker 的_recordWasMissing标记会保留后续章节通常会继续讲解渲染期间的数据请求fetchPolicy/renderPolicy如何与缓存复用协同本机制正是其中缓存命中判定的一环。小结missingFieldHandlers是 Relay 缓存复用体系中的一块拼图它把不同查询指向同一实体这一领域知识显式编码进 RelayEnvironment让 DataChecker 在宣布数据缺失前有机会用 Store 中已有的等价记录补齐缺失字段。本文给出的三种kind、handle四参签名、官方示例以及 DataChecker.js 的实现剖析足以支撑你在自己的应用中编写可靠、可复用的缺失字段处理器进一步提升缓存命中率、减少不必要的网络请求。【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考