ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Relay Derived Fields 派生字段完全指南:用 `@rootFragment` 构建可组合、全局记忆化的客户端计算图

Relay Derived Fields 派生字段完全指南:用 `@rootFragment` 构建可组合、全局记忆化的客户端计算图 前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载本篇技术指南以 Relay v18 官方文档「Derived Fields」为核心系统讲解 Relay Resolvers 中派生字段Derived Fields的定义方式、rootFragment与readFragment的协作机制、派生字段的依赖追踪与自动重算原理并结合当前仓库的编译器源码与运行时实现展开纵深分析。读完本文你将掌握如何把其他字段的纯函数建模进 GraphQL 图、如何让派生字段与普通字段一样在 schema 中被发现与复用以及如何通过组合与参数传递构建复杂的显式计算图。什么是 Derived Fields把计算也放进 GraphQL 图Relay Resolvers 除了用于建模客户端状态Client State还允许你定义一类特殊的字段——它们是其他字段的纯函数pure function。这类字段被称为派生字段Derived Fields并且可以定义在任何类型上无论该类型是服务端定义的还是客户端定义的。这一点与常规 Resolver 有本质区别常规 Resolver如 定义字段 所描述的 model resolver直接从父模型类型实例例如UserModel上读取数据并计算返回值而派生字段不直接接收模型实例它通过一个特殊的 GraphQL fragment 来声明自己依赖哪些图内数据Relay 编译器负责把 fragment 对应的数据传入解析函数。// 常规model-basedresolver直接读取模型实例 /** * RelayResolver User.name: String */ export function name(user: UserModel): string { return user.name; }// 派生derivedresolver通过 root fragment 读取图内数据 import {readFragment} from relay-runtime; /** * RelayResolver User.fullName: String * rootFragment UserFullNameFragment */ export function fullName(key: UserFullNameFragment$key): string { const user readFragment(graphql fragment UserFullNameFragment on User { firstName lastName } , key); return ${user.firstName} ${user.lastName}; }派生字段与普通字段在消费侧完全无差别产品代码使用useLazyLoadQuery、useFragment等熟悉的 Relay 数据获取 API 读取它无需关心该字段是服务端字段、客户端状态字段还是派生计算字段参见 Relay Resolvers 简介。为什么选择 Derived Fields与自定义 React Hooks 的五大优势对于全局相关globally relevant的数据相比自定义 React Hooks 这类替代方案派生字段具有显著优势。下面以表格对照说明优势Derived Fields 的机制对比 React Hooks全局记忆化Global memoizationRelay Resolvers 会自动记忆化派生字段的计算结果并且这个缓存被应用中所有组件共享Hooks 的记忆化useMemo是组件实例级、局部的高效更新Efficient updates如果派生 Resolver 被重新计算后得到的值与原值相同Relay 可以避免重渲染读取了该字段的组件Hooks 中值未变时仍需自行处理引用稳定性否则可能触发多余渲染可组合Composable派生字段可以与其他派生字段自由组合构建复杂但显式的计算图自定义 Hooks 之间的组合依赖手动串联关系隐含在代码中可发现Discoverable图内的值通过GraphQL schema 暴露开发者更可能发现并复用而不是重复实现Hooks 没有集中的可发现机制可文档化DocumentedGraphQL 的字段文档系统与结构化弃用deprecation模型让字段用途与预期用法清晰可查Hooks 的文档只能依赖代码注释与惯例其中全局记忆化是派生字段相对 Hooks 最核心的差异如果两个兄弟组件同时读取同一个派生字段该字段的计算只会执行一次缓存结果被两个组件共享。从源码结构看这一能力由运行时中的 Resolver 缓存如packages/relay-runtime/store/ResolverCache.js与依赖追踪机制共同支撑。需要说明Relay Resolvers 目前仍属实验性特性使用前需按 启用 Relay Resolvers 的指引开启相应 feature flag。定义一个 Derived ResolverrootFragmentreadFragment派生 Resolver 在写法上与其他 Resolver 几乎一致唯一区别在于它通过 root fragment 读取 GraphQL 数据而不是从父模型类型实例计算。第一步声明rootFragment在 resolver 函数的 docblock 中使用rootFragment标签后跟 fragment 的名称。这告诉 Relay 编译器请把该 fragment 的fragment key作为参数传给 resolver 函数。import {readFragment} from relay-runtime; /** * RelayResolver User.fullName: String * rootFragment UserFullNameFragment */ export function fullName(key: UserFullNameFragment$key): string { // ... }rootFragment指向的 fragment 必须定义在该字段所属的父类型上。例如User.fullName字段的 root fragment 必须是fragment ... on User。第二步用readFragment读取数据readFragment从relay-runtime导入。它接收两个参数graphql标签包裹的 fragment 定义以及传入的 fragment key返回 fragment 对应的数据对象export function fullName(key: UserFullNameFragment$key): string { const user readFragment(graphql fragment UserFullNameFragment on User { firstName lastName } , key); return ${user.firstName} ${user.lastName}; }依赖追踪与自动重算关键机制在于Relay 会追踪 resolver 从 fragment 中读取的所有值当其中任何一个值发生变化时自动重新计算该 resolver。这意味着你完全不需要手动记忆化memoizeresolver 函数——声明式的依赖关系由 Relay 全权管理。这一读取即追踪的实现可以在运行时源码中得到印证。在 packages/relay-runtime/store/ResolverFragments.js 中readFragment通过上下文栈获取当前 resolver 的上下文contextStack[contextStack.length - 1]然后调用context.getDataForResolverFragment(fragmentSelector, fragmentKey)读取数据——正是这个调用把读了哪些字段记录进依赖集合function readFragment(fragmentInput, fragmentKey) { if (!contextStack.length) { throw new Error( readFragment should be called only from within a Relay Resolver function., ); } const context contextStack[contextStack.length - 1]; const fragmentNode getFragment(fragmentInput); const fragmentSelector getSelector(fragmentNode, fragmentKey); // ... const {data, isMissingData, fieldErrors} context.getDataForResolverFragment( fragmentSelector, fragmentKey, ); // ... return data; }值得注意的边界行为readFragment只允许在 Relay Resolver 函数内部调用否则会直接抛错readFragment should be called only from within a Relay Resolver function.当 fragment 数据缺失或字段报错时它会抛出哨兵值RESOLVER_FRAGMENT_ERRORED_SENTINEL交由上层处理错误语义。组合Composition把计算图显式地搭起来派生字段最强大的特性之一是可以读取其他 Relay Resolver 字段。这意味着你可以定义一个派生字段其输入同时包含服务端数据、客户端数据甚至其他派生字段从而构建出复杂但完全显式的计算图。官方文档给出了一个经典的购物车校验示例/** * RelayResolver CheckoutItem.isValid: Boolean * rootFragment CheckoutItemFragment */ export function isValid(key): boolean { const item readFragment(graphql fragment CheckoutItemFragment on CheckoutItem { product { price } quantity } , key); return item.product.price * item.quantity 0; } /** * RelayResolver ShoppingCart.canCheckout: Boolean * rootFragment ShoppingCartFragment */ export function canCheckout(key): boolean { const cart readFragment(graphql fragment ShoppingCartFragment on ShoppingCart { items { isValid } } , key); return cart.items.every(item item.isValid); }让我们拆解这个示例中隐含的计算图CheckoutItem.isValid是一个底层派生字段它只依赖服务端字段product.price与quantity计算单价 × 数量 0ShoppingCart.canCheckout是一个组合派生字段它的 root fragment 里没有直接读取price或quantity而是读取了items { isValid }——isValid正是上面定义的另一个 Resolver 字段由此形成两层计算依赖canCheckout→isValid→price、quantity。当任意底层服务端字段变化时isValid自动重算进而触发canCheckout的重新计算。这种字段读字段的模式完全贴合 GraphQL 的字段选择语法计算关系被显式地编码在 fragment 选择集中任何开发者都能一眼看出canCheckout依赖什么而不是像自定义 Hooks 那样需要追踪隐式的调用链。编译器层面rootFragment与派生字段依赖的展开由多个 transform 协作完成。在 compiler/crates/relay-transforms/src/relay_resolvers/fragment_dependencies.rs 中处理 resolver 的 fragment 依赖而 compiler/crates/relay-transforms/src/generate_relay_resolvers_root_fragment_split_operation.rs 会为带 root fragment 的 resolver 生成拆分操作split operation确保解析函数所依赖的数据在查询中被正确获取并绑定。给rootFragment传递参数如果派生 Resolver 的 root fragment 中某个字段需要参数你可以通过给 docblock 添加arguments标签来声明。argument标签接收参数名与参数类型且参数类型必须是合法的 GraphQL 输入类型GraphQL input type。关于参数与 Resolver 的更多细节参见 字段参数 Field Arguments。与运行时参数Runtime Arguments的区分在讨论 fragment 参数之前先明确两种参数的区别。运行时参数直接在字段定义中声明并作为 resolver 函数的第二个参数读取/** * RelayResolver User.greet(salutation: String!): String */ export function greet(user: UserModel, args: { salutation: string }): string { return ${args.salutation}, ${user.name}!; }消费端查询需显式传参query MyQuery($salutation: String!) { me { greet(salutation: $salutation) } }派生字段的 fragment 参数使用argumentDefinitions而当定义的是派生 Resolver且 root fragment 中某个字段需要参数时field-arguments 指南给出的完整做法是在 fragment 定义中使用argumentDefinitions显式声明 fragment 参数该指令的完整语法见 GraphQL 指令参考argumentDefinitions此时 resolver 字段会期望该参数作为字段参数被传入/** * RelayResolver User.fancyGreeting: String * rootFragment UserFancyGreetingFragment */ export function fancyGreeting(key: UserFancyGreetingFragment$key): string { const user readFragment(graphql fragment UserFancyGreetingFragment on User argumentDefinitions( salutation: {type: String}, ) { name greet(salutation: $salutation) } , key); return ${user.name} says ${user.greet}; }这里argumentDefinitions声明了 fragment 参数salutation类型String然后通过变量$salutation把它转发给需要参数的greet字段。消费端查询同样需要把参数传给 resolver 字段query MyQuery($salutation: String!) { me { fancyGreeting(salutation: $salutation) } }argumentDefinitions的完整语法支持可选参数带defaultValue、必选参数以及 provided variables由 provider 函数在运行时提供值fragment TodoList_list on TodoList argumentDefinitions( count: {type: Int, defaultValue: 10} # 可选参数带默认值 userID: {type: ID} # 必选参数 ) { title todoItems(userID: $userID, first: $count) { ...TodoItem_item } }参数校验的编译器测试仓库中relay-docblockcrate 的解析测试覆盖了 fragment 参数的各种合法与非法形态。例如 relay-resolver-with-field-and-fragment-args.js 验证了字段参数 fragment 参数共存的解析relay-resolver-with-field-and-fragment-args.invalid.js 则用于断言冲突参数声明会被编译器拒绝。编译器如何解析rootFragment为了深入理解rootFragment的底层处理我们看一下编译器侧的 IR 结构。在 compiler/crates/relay-docblock/src/ir.rs 中定义了RootFragment结构pub struct RootFragment { fragment: WithLocationFragmentDefinitionName, generated: bool, // 对于 Model Resolver需要把 fragment 数据中的 id 或 __relay_model_instance // 字段传给 resolver 函数 inject_fragment_data: OptionFragmentDataInjectionMode, }它记录了三类关键信息被引用的 fragment 名称fragment、该 fragment 是否为编译器自动生成generated、以及是否需要把 fragment 数据中的特定字段注入 resolverinject_fragment_data主要用于 id /__relay_model_instance这类场景。在解析测试的 expected 输出中可以看到rootFragment被解析为TerseRelayResolverIr.root_fragment字段见 relay-resolver-with-fragment.expecteddocblock 中的rootFragment myRootFragment被解析为Some(WithLocation { item: FragmentDefinitionName(myRootFragment) })同时source_hash会被记录下来用于产物缓存。对应的输入 fixture relay-resolver-with-fragment.js 展示了最小写法一个 docblock 携带RelayResolver User.my_field: RelayResolverValue与rootFragment myRootFragment随后在同一个文件的graphql标签中定义该 fragment/** * RelayResolver User.my_field: RelayResolverValue * rootFragment myRootFragment */ graphql fragment myRootFragment on User { name } 值得注意root fragment 既可以内联定义在 resolver 所在文件中如上也可以在任何 resolver 文件中定义fragment 名称全局可见。另外在relay-docblock的 to_schema 测试中还有一类针对 Model 类型自动生成 root fragment 的场景见 terse-relay-resolver-with-root-fragment-on-model.js此时 fragment 由编译器根据rootFragment声明自动生成。派生字段的应用场景与注意事项典型适用场景结合 Relay Resolvers 简介 与本文内容派生字段特别适合以下场景跨组件共享的计算结果多个兄弟组件需要同一份格式化/聚合结果如fullName、isValid利用全局记忆化避免重复计算服务端数据 客户端数据 派生数据的组合例如商品价格服务端× 数量客户端本地状态 0这类跨数据来源的校验希望把业务计算schema 化让计算字段出现在 GraphQL schema 中配合字段文档description与弃用deprecated机制形成团队内可发现、可复用的计算资产。注意事项实验性特性Relay Resolvers含派生字段当前为实验特性需先启用对应 feature flagAPI 未来可能调整纯函数约束派生字段应当是其他字段的纯函数应避免在 resolver 内部执行副作用如修改外部状态、发起网络请求否则将破坏依赖追踪与记忆化的语义依赖追踪基于读取只有通过readFragment实际读取的字段才会被纳入依赖集合并触发重算因此不要在 resolver 中读取了却不用的字段上依赖隐式行为参数类型限制传给 root fragment 的参数类型必须是合法的 GraphQL 输入类型fragment 必须匹配父类型rootFragment引用的 fragment 必须定义在该字段所属的父类型上否则编译器会报错仓库中terse-relay-resolver-fragment-type-does-not-match-parent.invalid等测试即用于校验此类错误。深入学习路径如果你希望继续深入 Relay Resolvers 生态推荐按以下顺序阅读当前仓库的文档Relay Resolvers 简介特性总览与启用方法定义字段model-based resolver 基础写法字段参数运行时参数与 fragment 参数的完整对比返回类型resolver 可返回的字段类型实时字段随时间变化的值如何建模定义类型客户端类型建模也可以直接阅读运行时与编译器实现以验证本文涉及的原理ResolverFragments.jsreadFragment实现、ir.rsdocblock IR 与RootFragment结构、fragment_dependencies.rsfragment 依赖处理以及 generate_relay_resolvers_root_fragment_split_operation.rsroot fragment 拆分操作生成。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay Resolvers 完全指南在 Relay 15 中用客户端字段构建响应式派生状态Relay Resolvers 完全指南在 Relay 15 中用客户端字段构建响应式派生状态 Relay Resolvers 是 Relay 的一个实验性特前端开发工具Relay Resolvers 完全指南用客户端 Resolver 字段在 Relay 中建模响应式派生状态Relay Resolvers 完全指南用客户端 Resolver 字段在 Relay 中建模响应式派生状态 本篇指南围绕 Relay 的实验性特性 Rela前端开发工具Relay Resolvers 完全指南用 docblock 定义响应式客户端派生字段Relay Resolvers 完全指南用 docblock 定义响应式客户端派生字段 Relay Resolvers 是 Relay 提供的一项实验性特性前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表