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提供,否则编译器报错。 - 一个参数定义不能同时指定
provider和defaultValue(详见下文 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 分两步:
- 在
@argumentDefinitions中为某个参数添加provider: "[JSModule].relayprovider"字段:
fragment TodoItem_item on TodoList @argumentDefinitions( include_timestamp: { type: "Boolean!", provider: "Todo_ShouldIncludeTimestamp.relayprovider" }, ) { timestamp @include(if: $include_timestamp) text }- 确保
[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 供给。 - 参数定义不能同时指定
provider和defaultValue。 - 不稳定特性(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),同时校验不同片段对同一模块名声明了冲突类型/类型不一致的情况(ProvidedVariableConflictingModuleNames、ProvidedVariableConflictingTypes,见 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_fragment、transform_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]):指定分页时需要一起传给服务器的参数列表(如orderBy、isViewerFriend),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_selections、build_page_info_selections); - 把
@connection转换为内部的 handle field 指令(build_handle_field_directive_from_connection_directive),从而在运行时由 connection handler 维护分页状态。
四、@refetchable(queryName: String!, directives: [String], preferFetchable: Boolean):自动生成重查 query
useRefetchableFragment与usePaginationFragment都要求片段带@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.rs(Node类型走node(id:))、viewer_query_generator.rs(Viewer类型)、query_query_generator.rs(Query类型)以及fetchable_query_generator.rs(preferFetchable: 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的参数并读取plural与mask两个布尔参数(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的地方直接包含id与name字段,而不是 Relay 默认的掩码行为。
源码中@relay(mask: false)的判断逻辑集中在 relay_directive.rs:is_unmasked_fragment_spread与is_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_connections与required_directive、transform_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
相关推荐
Relay GraphQL 指令完全指南:@arguments、@connection、@refetchable、@relay、@required 与 @inline 详解
Relay GraphQL 指令完全指南:@arguments、@connection、@refetchable、@relay、@required 与 @inl
前端开发工具Relay 的 GraphQL 指令(Directives)完全指南:从 @arguments 到 @relay 的编译期语义与运行时行为
Relay 的 GraphQL 指令(Directives)完全指南:从 @arguments 到 @relay 的编译期语义与运行时行为 GraphQL 指令
前端开发工具Relay GraphQL 指令(Directives)完全指南:从 @arguments 到 @required 的编译期语义与运行时行为
Relay GraphQL 指令(Directives)完全指南:从 @arguments 到 @required 的编译期语义与运行时行为 Relay 通过一
前端开发工具