Relay GraphQL 指令(Directives)完全指南:@arguments、@connection、@refetchable 等 10 大指令的 API 参考与编译原理
2026/9/23 3:07:58 网站建设 项目流程

Relay 通过 GraphQL 指令(directive)为文档添加额外信息,供 Relay 编译器生成对应的运行时产物(runtime artifacts)。这些指令只存在于应用代码中,发送到 GraphQL 服务器的请求里会被移除。本篇指南以 Relay v17 官方 API 参考文档 为骨架,逐一讲解@arguments@argumentDefinitions@connection@refetchable@relay@required@alias@inline@waterfall的语义、参数、用法与限制,并结合仓库内编译器源码(compiler/crates/relay-transforms/)说明每个指令在编译管线中的真实处理方式。读完本文,你将能正确地在项目中书写与调试这些指令,并理解它们为何如此设计。

关于服务器支持的指令:Relay 编译器会保留 GraphQL 服务器自身支持的指令(如@include@skip),使它们继续成为发送给服务器的请求的一部分,并且不会改变生成的运行时产物。换句话说,Relay 只"消费"本文介绍的这些自有指令,其余指令原样透传。

一、@arguments:向片段传递参数

@arguments用于向一个用@argumentDefinitions定义了形参的片段传入实参。它必须应用在**片段展开(fragment spread)**上:

query TodoListQuery($userID: ID) { ...TodoList_list @arguments(count: $count, userID: $userID) # 在这里传参 }
  • 参数值可以是操作(query/mutation/subscription)的顶层变量(如$count$userID),也可以是字面量。
  • 只有片段用@argumentDefinitions声明过的形参才允许被传入;传入未声明参数属于编译错误。

在编译器实现中,@arguments由 apply_fragment_arguments.rs 处理。该 transform 会收集每个片段展开处的实参,将其"拼接"进片段内部,最终把局部变量解析为根操作变量或字面量;若父级尝试通过@arguments传入 provided variable(见下文),编译器会报ProvidedVariableIncompatibleWithArguments校验错误(apply_fragment_arguments.rs 附近的校验逻辑)。

二、@argumentDefinitions:声明片段形参

@argumentDefinitions@arguments的配套指令,用于声明片段接受哪些参数。它应用在**片段定义(fragment definition)**上:

fragment TodoList_list on TodoList @argumentDefinitions( count: {type: "Int", defaultValue: 10}, # 可选参数(有默认值) userID: {type: "ID"}, # 必填参数(无默认值) ) { title todoItems(userID: $userID, first: $count) { # 在片段内部把形参当作变量使用 ...TodoItem_item } }

关键规则:

  • 每个参数项是一个对象字面量,含type(GraphQL 类型字符串)和可选的defaultValue
  • 形参在片段内部可以像普通 GraphQL 变量一样以$name形式使用。
  • 可选参数(带defaultValue)可以不传;必填参数(无默认值)在父级展开时必须通过@arguments提供,否则编译器报错。
  • 一个参数定义不能同时指定providerdefaultValue(详见下文 provided variables 的约束)。

在编译器内部,@argumentDefinitions的实参解析与变量类型推断由 root_variables.rs 中的InferVariablesVisitor完成:它会沿片段展开链路传递地收集每个片段引用的根变量,并为每个变量计算"最具体"的类型(即保证查询合法所需的最小类型约束),见 root_variables.rs 中infer_operation_variables/infer_fragment_variables的注释说明。

Provided Variables:由 Provider 函数供给值的片段变量

provided variable 是一种特殊的片段变量,其值在运行时由指定的 provider 函数提供。它的典型用途是向片段注入设备属性、用户实验开关(experiment flags)等运行时常量,避免每次手写顶层变量与传参。

添加 provided variable 分两步:

  1. @argumentDefinitions中为某个参数添加provider: "[JSModule].relayprovider"字段:
fragment TodoItem_item on TodoList @argumentDefinitions( include_timestamp: { type: "Boolean!", provider: "Todo_ShouldIncludeTimestamp.relayprovider" }, ) { timestamp @include(if: $include_timestamp) text }
  1. 确保[JSModule].relayprovider.js文件存在并导出get()函数;get()给定的某次运行期间必须每次都返回相同的值:
// Todo_ShouldIncludeTimestamp.relayprovider.js export default { get(): boolean { // 某次运行内必须恒为 true 或恒为 false return check('todo_should_include_timestamp'); }, };

约束与注意事项(针对 OSS 版本):

  • 即使片段在@argumentDefinitions中声明了 provided variable,其父级也不能通过@arguments传入provided variable——该值只能由 provider 供给。
  • 参数定义不能同时指定providerdefaultValue
  • 不稳定特性(unstable / subject to change):Relay 会把 provided variables 转换成 operation 根变量,并重命名为__relay_internal__pv__[JsModule]。只有在你调试包含 provided variable 的查询时才需要关心这个内部命名。

从源码看,provided variable 的处理链路是:

  • provided_variable_fragment_transform.rs 中的ProvidedVariableFragmentTransform负责把片段内的局部 provided variable 替换为全局重命名后的变量(transform_variable见 L198-L210),同时校验不同片段对同一模块名声明了冲突类型/类型不一致的情况(ProvidedVariableConflictingModuleNamesProvidedVariableConflictingTypes,见 L249-L257)。
  • 重命名的前缀常量__relay_internal定义在 util.rs#L153-L154,拼接格式(__relay_internal__pv__[JsModule])由format_provided_variable_name实现(util.rs#L192-L213)。
  • provided variable 会携带一个内部元数据指令(ProvidedVariableMetadata::directive_name()),后续refetchable_fragmenttransform_connections等 transform 会把它从"需要用户传参"的变量集合中过滤掉(参见 utils.rs 与 transform_connections.rs),因为其值由 provider 自动提供,不需要进入 query 的显式变量列表。

三、@connection(key: String!, filters: [String]):连接字段标注

在使用usePaginationFragment做分页时,Relay 要求对 connection 字段标注@connection指令,用于把分页所需元数据(edge、pageInfo 的生成)附加到字段上:

fragment FriendsListComponent_user on User { friends(first: 10) @connection(key: "FriendsListComponent_user_friends", filters: []) { edges { node { id } } } }
  • key必填,String!):全局唯一标识该连接,Relay 用它把多个分页结果缓存在 store 中的同一位置。规范要求 key 取组件名_变量名_字段名形式。
  • filters(可选,[String]):指定分页时需要一起传给服务器的参数列表(如orderByisViewerFriend),Relay 会基于 key+filters 计算连接的缓存键,确保不同过滤条件下的结果不会相互污染。默认值为空数组。

更完整的用法与示例请参阅 渲染 Connections 指南。

在编译管线中,@connection由 transform_connections.rs 的transform_connections处理(在 apply_transforms.rs#L181-L182 被调用)。它执行:

  • 解析@connection参数,构建连接元数据(ConnectionMetadata),并校验连接字段的选择集必须满足连接规范(assert_connection_selections);
  • 依据 schema 中 connection 的游标、节点等字段名(由ConnectionInterface抽象,见 connection_util.rs)生成 edge / pageInfo 的选择集与内部 reader 片段(build_edge_selectionsbuild_page_info_selections);
  • @connection转换为内部的 handle field 指令(build_handle_field_directive_from_connection_directive),从而在运行时由 connection handler 维护分页状态。

四、@refetchable(queryName: String!, directives: [String], preferFetchable: Boolean):自动生成重查 query

useRefetchableFragmentusePaginationFragment都要求片段带@refetchable指令。该指令只能加在"可重查(refetchable)"的片段上,即片段声明在:

  • Viewer类型上,或
  • Query类型上,或
  • 实现了Node接口的类型上(即该类型有id)。

@refetchable会让编译器按指定的queryName自动生成一个查询(query),并同时生成对应的 Flow 类型,可从生成文件<queryName>.graphql.js中导入。

可选参数:

  • directives: [String]:向自动生成的 query 追加指令列表。例如测试场景中追加@relay_test_operation指令(参见 测试 Relay 组件指南)。
  • preferFetchable: Boolean:当片段类型实现了Node接口时,指示编译器优先生成fetch_MyType(): MyType形式的查询,而非node(id: $id)查询。这对于已采用@strong/@fetchable服务端标注的 schema 很有用——可以直接按具体类型抓取对象,而无需先把Node接口细化(refine)到具体类型。

示例:

graphql` fragment FriendsListComponent_user on User @refetchable( queryName: "FriendsListFetchQuery" directives: ["@relay_test_operation"] ) { ... } `

更详细的用法与示例参见 useRefetchableFragment 与 usePaginationFragment。

源码层面,@refetchable的参数解析集中在 refetchable_directive.rs:

  • queryName必须是字符串字面量,否则报ExpectQueryNameToBeString诊断(L67-L77);
  • directives必须是字符串字面量列表,每个字符串会被graphql_syntax::parse_directive解析并构建为真实指令(L78-L129);
  • preferFetchable必须是常量布尔值,否则报ExpectPreferFetchableToBeConstantBoolean(L130-L144)。

随后 refetchable_fragment.rs 依据片段声明所在类型,选择不同的 query 生成器:node_query_generator.rsNode类型走node(id:))、viewer_query_generator.rsViewer类型)、query_query_generator.rsQuery类型)以及fetchable_query_generator.rspreferFetchable: true时生成fetch_MyType())。

五、@relay(plural: Boolean):声明列表片段

在为 Fragment container 定义片段时,可用@relay(plural: true)声明该片段对应的 prop 是一组对象而非单个对象。要求:展开@relay(plural: true)片段的父级查询/片段,必须把该展开放在一个**多值字段(即由 GraphQL 列表支持的字段)**内:

// 列表片段定义 graphql` fragment TodoItems_items on TodoItem @relay(plural: true) { id text } `; // 列表片段的使用:注意父级类型是一个条目列表(TodoItem[]) fragment TodoApp_app on App { items { // 这里的父类型是列表 ...TodoItem_items } }

编译器中@relay指令的参数解析位于 relay_directive.rs:RelayDirective::find会遍历@relay的参数并读取pluralmask两个布尔参数(L49-L74);plural标志随后被 relay-codegen 用于生成数组类型的 Flow/TS 类型(对应TodoItems_items的 prop 类型为Array<TodoItem_items>)。

六、@required:声明字段为空值时的运行时行为

@required用于在 Relay 查询中声明字段为 null 时运行时应如何处理(例如直接抛错或仅记录日志)。它帮助你提前暴露数据质量问题,避免静默的 undefined 下钻。

fragment UserSummary_user on User { name email @required(action: THROW) profile_picture { uri @required(action: LOG) } }

action可选值(由编译器内部常量定义,见 required_directive.rs#L57-L61):

  • THROW:读取到 null 时抛出错误;
  • LOG:读取到 null 时记录日志;
  • NONE:不采取任何动作;
  • DANGEROUSLY_THROW_ON_SEMANTICALLY_NULLABLE_FIELD:对语义上本可为空的字段也强制抛错(高风险选项)。

完整语义与最佳实践请参阅 @required 指令指南。

编译器在 required_directive.rs 中实现该指令:required_directive(program, feature_flags)(L72-L87)遍历整棵选择树,记录每个字段的"required 路径"(path)与动作,并生成RequiredMetadataDirective元数据(L65-L70)附加到字段上;运行时据此在 null 出现时执行对应的 THROW / LOG 行为。同时它还会校验@required不能出现在抽象类型(interface/union)的内联片段中(WithinAbstractInlineFragment校验,见 L142-L150)。

七、@alias(as: String):给片段展开取别名

@alias允许给**片段展开(fragment spread)或内联片段(inline fragment)**起别名,类似字段别名(field alias)。它在以下场景很有用:想条件性地包含一个片段、然后检查它是否真的被拉取,或者把数据按别名分组。

  • 对于片段展开,别名默认取片段名
  • 对于内联片段,别名默认取类型名
  • 如果你想起自定义名字,或你的内联片段没有类型条件(type condition),可以用as参数显式指定。
fragment MyFragment on User { ... on User @alias(as: "myGreatAlias") { name } }

实现层面,fragment_alias_directive.rs 中的FragmentAliasTransform负责处理@alias:它把别名、类型条件、选择集类型等信息记录到FragmentAliasMetadata元数据(L45-L53),随后由remove_aliased_inline_fragments(L70-L75,在 apply_transforms.rs#L367-L368 被调用)把别名的内联片段展开成带 key 的选择集,保证读取时能按别名拿到数据。更多语义见 @alias 指令指南。

八、@inline:在渲染期之外读取数据

Relay 的 hooks API 只允许你在渲染阶段从 store 读取数据。如果需要在渲染期之外(或脱离 React 环境)读取数据,Relay 提供@inline指令:标注了@inline的片段,其数据可以用readInlineData读取。

典型场景:某个非 React 工具函数需要一组特定字段,所有使用它的组件都应展开该@inline片段,确保数据被完整加载:

import {graphql, readInlineData} from 'react-relay'; // 从 React 中调用的非 React 函数 function processItemData(itemRef) { const item = readInlineData(graphql` fragment processItemData_item on Item @inline { title price creator { name } } `, itemRef); sendToThirdPartyApi({ title: item.title, price: item.price, creatorName: item.creator.name }); }
export default function MyComponent({item}) { function handleClick() { processItemData(item); } const data = useFragment( graphql` fragment MyComponent_item on Item { ...processItemData_item title } `, item ); return ( <button onClick={handleClick}>Process {item.title}</button> ); }

要点:

  • @inline标注的片段在运行时产生的是reader类型的 fragment(即数据已内联进父级读取结构),调用方通过 readInlineData 直接取出这段数据,不再经过组件渲染的数据掩码(mask)流程。
  • @inline与下文@relay(mask: false)的定位有重叠,但官方明确推荐使用@inline替代@relay(mask: false)
  • @inline片段内部仍然可以再展开其他普通片段(如...processItemData_item),形成数据依赖的组合。

九、@relay(mask: Boolean):关闭数据掩码(不推荐)

不推荐使用@relay(mask: false),请优先考虑使用@inline片段。

@relay(mask: false)的作用是阻止数据掩码:当一个片段展开带上@relay(mask: false)时,它的数据会直接暴露给父级,而不是被掩码成仅供自身容器读取。

  • 作用于片段定义时,@relay(mask: false)会把生成的 Flow 类型改成更适合以同样指令展开时使用的形式:类型不再是精确对象(exact object),且不再包含内部标记字段
  • 在处理单个组件内部的嵌套/递归数据时,这可以减少冗余片段的编写。
  • 但请注意:跨多个容器共享单个片段通常被认为是反模式(anti-pattern),滥用此指令可能导致应用中过度拉取(over-fetching)
graphql` fragment Component_internUser on InternUser @relay(mask: false) { id name } `;

如上例,userprop 将在任何展开...Component_internUser的地方直接包含idname字段,而不是 Relay 默认的掩码行为。

源码中@relay(mask: false)的判断逻辑集中在 relay_directive.rs:is_unmasked_fragment_spreadis_unmasked_fragment_definition(L38-L44)用于识别"未掩码"的展开与定义;RelayDirective::find解析时把mask取反存为unmask(L54-L58),后续 codegen 依据该标志决定是否生成带内部标记字段的精确类型。

十、@waterfall:标注懒加载的服务端类型边

在使用 Relay Resolvers 时,可以创建指向服务端类型的客户端自定义边(client-defined edge)。当读取这些边字段时,Relay 被迫惰性拉取(lazily fetch)该边的服务端数据——这会导致 Relay 额外发起第二次请求来获取边的数据。

为了在编辑器和代码评审中突出这一"代价",Relay 编译器要求所有对该类字段的读取都必须标注@waterfall

fragment EditPost on DraftPost { author @waterfall { name } }
  • @waterfall不接收参数,直接修饰字段选择。
  • 它既是对开发者的显式提示(此处会产生瀑布式请求),也让编译器在缺失标注时给出诊断,防止无意引入二次请求。

更多细节参见 Relay Resolvers 指南中的 返回类型(Return Type)——服务端类型 一节,其中说明了服务端类型与@waterfall的配合方式。

十一、指令处理管线一览:这些指令何时被"消费"

上述指令的编译处理顺序可从 apply_transforms.rs 中看到(每个阶段都有对应的日志计时点):

  • transform_connections:解析@connection、生成分页元数据与 edges/pageInfo 结构(L181-L182);
  • transform_refetchable_fragment:根据@refetchable生成可重查查询(L207-L208);
  • required_directive:解析@required并附加空值处理元数据(L276-L277);
  • remove_aliased_inline_fragments:把@alias的内联片段转换为带 key 的选择集(L367-L368);
  • inline_fragments:执行内联片段展开(L504);
  • typegen 管线中会再次执行transform_connectionsrequired_directivetransform_refetchable_fragment(L696-L768),以生成与运行时产物一致的类型。

记忆要点:所有 Relay 自有指令都是"编译期指令"——它们只影响编译器生成产物(查询文本、normalization 节点、Flow/TS 类型),不会出现在发往服务器的 GraphQL 请求中;而@include@skip等服务端指令则被透传保留。正确区分这两类指令,是理解 Relay 指令模型的关键。

结语

@arguments/@argumentDefinitions让片段获得参数化能力,@connection@refetchable支撑起分页与重查两大高频场景,@relay(plural)/@relay(mask)控制片段的数据形态与掩码策略,@required细化空值语义,@inline@alias则分别解决渲染期外读数与数据分组问题,@waterfall为 Relay Resolvers 的惰性加载代价提供显式标注。理解每个指令的"编译期消费点",能帮助你在排查产物差异、优化查询文本与调试类型生成时直击要害。

  • 前端
  • 开发工具

【免费下载链接】relay

Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay

点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询