Relay 查询变量实战指南:从 GraphQL 变量到 @arguments 与 @argumentDefinitions
2026/9/23 15:47:51 网站建设 项目流程

本篇指南以 Relay(Meta 开源的 JavaScript 数据驱动 React 应用框架)官方文档中关于查询变量(Query Variables)的讲解为骨架,系统梳理 GraphQL 变量在 Relay 中的三种用法:查询级全局变量、fragment 对全局变量的引用,以及通过@arguments/@argumentDefinitions声明的 fragment 局部变量。文中结合本仓库编译器与运行时源码,深入解释变量类型推导、fragment 参数内联(静态柯里化)等底层机制,帮助读者在组件化开发中正确设计可复用、可定制、可被编译器静态校验的 Relay 数据依赖。

从 GraphQL 变量说起:查询中的动态输入

在前面的示例中你可能已经注意到,GraphQL 查询声明里出现了$id这样的符号——这正是 GraphQL Variables(GraphQL 变量)的语法。GraphQL 变量是 GraphQL 提供的一种构造,允许在查询内部引用动态值。

UserQuery为例:

query UserQuery($id: ID!) { # $id 的值被用作 user() 调用的输入: user(id: $id) { id name } }

这里ID!$id变量的类型,表示它是一个必填(non-null)的 ID。当向服务器发送网络请求以获取上述查询时,需要同时提供两样东西:

  1. 查询本身;
  2. 本次执行该查询要使用的变量集合。

例如:

# 查询: query UserQuery($id: ID!) { # ... } # 变量: {"id": 4}

从服务器获取上述查询与变量,会产生如下响应:

{ "data": { "user": { "id": "4", "name": "Mark Zuckerberg" } } }

可以看到,服务器解析$id变量、执行user(id: 4),并在返回结果前将其应用于查询的各个位置。这正是 GraphQL 变量区别于"把值硬编码进查询字符串"的价值:同一份查询模板可以通过不同的变量值反复复用,同时请求内容保持可缓存、可校验。

Fragment 引用查询级全局变量

变量不只可以在查询的顶层参数列表中使用。Fragment 同样可以引用由查询声明的变量:

fragment UserFragment on User { name profile_picture(scale: $scale) { uri } } query ViewerQuery($scale: Float!) { viewer { actor { ...UserFragment } } }

关于这种跨 fragment 的变量引用,官方文档给出了三条关键规则:

  • 尽管UserFragment并没有"声明"$scale变量,它仍然可以直接引用它。任何直接或间接包含该 fragment 的查询,都必须声明该变量及其类型,否则会产生错误。
  • 换句话说,查询变量对该查询的所有后代 fragment 全局可见
  • 一个引用了全局变量的 fragment,只能被(直接或间接)定义了该全局变量的查询所包含。

这意味着 fragment 一旦使用了全局变量,它就与"某个声明了该变量的查询"形成了强耦合——fragment 本身无法独立决定该变量的存在性,只能依赖包含它的查询来完成声明。

Relay 组件中的 fragment 变量引用与编译期检查

在 Relay 中,组件内部声明的 fragment 同样可以引用查询变量:

function UserComponent(props: Props) { const data = useFragment( graphql` fragment UserComponent_user on User { name profile_picture(scale: $scale) { uri } } `, props.user, ); return (...); }

这里有两个要点需要理解:

  • 上述 fragment 可能被多个查询包含、被不同组件渲染,这意味着任何最终渲染/包含该 fragment 的查询,都必须声明$scale变量。
  • 如果某个恰好包含该 fragment 的查询没有声明$scale变量,Relay 编译器会在**构建期(build time)**直接报错,从而保证一个不合法(缺少变量声明)的查询永远不会被发送到服务器——服务器收到这类查询同样会报错,但把检查前移到编译期显然更安全、更早暴露问题。

编译器如何推断查询必须声明的变量

"查询必须声明其包含的 fragment 所引用的所有变量"这一规则,在编译器中有专门的实现。从源码看,root_variables.rs 中的InferVariablesVisitor/VariablesVisitor会遍历整个 program:

  • 遍历每个 operation,收集其**传递引用(transitively)**的所有根变量,即该 fragment 自身及其展开的所有 fragment 用到的根变量的并集;
  • 每个变量记录的是使用它的最具体类型is_type_strict_subtype_of严格子类型判断),以保证查询声明出的类型对所有使用位置都合法;
  • 当同一变量以不兼容类型被多处使用时,会抛出IncompatibleVariableUsage诊断错误。

同时,fragment 的遍历结果会被缓存(visited_fragments),一旦某个 fragment 被计算过,其他查询直接复用,避免重复处理。这正是"任何包含该 fragment 的查询都必须声明这些变量"这一规则在实现层面的保证。

@arguments 与 @argumentDefinitions:fragment 局部变量

全局查询变量的耦合问题引出了 Relay 的解决方案:Relay 提供了使用@arguments@argumentDefinitions指令来声明作用域限定在 fragment 内部的局部变量。使用局部变量的 fragment 易于定制和复用,因为它们不依赖全局(查询级)变量的值。

声明带参数的 fragment:@argumentDefinitions

/** * 用 @argumentDefinitions 声明一个接受参数的 fragment */ function TaskView(props) { const data = useFragment( graphql` fragment TaskView_task on Task @argumentDefinitions(showDetailedResults: {type: "Boolean!"}) { name is_completed ... @include(if: $showDetailedResults) { description } } `, props.task, ); }

@argumentDefinitions为 fragment 声明了局部变量showDetailedResults,其类型为Boolean!。fragment 内部可以像使用普通变量一样在字段参数、指令条件中使用它——上例中@include(if: $showDetailedResults)会根据该局部变量的值决定description字段是否被包含。

传入参数:@arguments

/** * 用 @arguments 包含 fragment */ function TaskList(props) { const data = usePreloadedQuery( graphql` query TaskListQuery { todays_tasks { ...TaskView_task @arguments(showDetailedResults: true) } tomorrows_tasks { ...TaskView_task @arguments(showDetailedResults: false) } } `, props.queryRef, ); }

同一个TaskView_taskfragment 被展开两次,分别传入truefalse,实现了"同一模板、不同渲染"的定制化复用。局部变量带来的收益是结构性的:

  • 查询定义必须列出所有被嵌套 fragment(包括递归嵌套的 fragment)使用的变量。这是全局变量方案的固有成本。
  • 由于一个 fragment 可能被很多查询访问,修改一个使用全局变量的 fragment,往往需要同步修改大量查询定义。
  • 这还可能催生尴尬的"同义变量"混乱,例如同时存在$showDetailedResults$showDetails两种写法。

而只使用局部变量的 fragment 不涉及全局变量,天然绕开上述所有问题。

@arguments 可以传什么

向 fragment 传递@arguments时,可以传入:

  • 字面量,例如42.0
  • 另一个变量,它可以是:
    • 查询级全局变量;
    • @argumentDefinitions声明的局部变量;
    • 或直接的字面量值。

TaskView_task真正作为查询的一部分被获取时,showDetailedResults的值将取决于其父级为TaskView_task提供的参数。

局部变量同样遵循"传递可见"规则

需要特别注意的是,@arguments传入的值可以是外层 fragment 的局部变量,而 fragment 内部依然遵守"变量对后代可见"的规则——一个 fragment spread 只要(直接或间接)引用了某个局部变量,包含它的父级就必须通过@arguments提供该值。这与全局变量的"必须声明"约束在精神上完全一致,只是作用域从查询级收窄到了 fragment 级。

默认值:让参数变为可选

期望接受参数的 fragment 还可以声明默认值,使参数变为可选:

/** * 声明一个带默认值参数的 fragment */ function TaskView(props) { const data = useFragment( graphql` fragment TaskView_task on Task @argumentDefinitions(showDetailedResults: {type: "Boolean!", defaultValue: true}) { name is_completed ... @include(if: $showDetailedResults) { description } } `, props.task, ); }
function TaskList(props) { const data = usePreloadedQuery( graphql` query TaskListQuery { todays_tasks { ...TaskView_task } tomorrows_tasks { ...TaskView_task @arguments(showDetailedResults: false) } } `, props.queryRef, ); }

这里showDetailedResults声明了defaultValue: true

  • todays_tasks的展开没有传@arguments,则$showDetailedResults使用默认值truedescription会被包含;
  • tomorrows_tasks显式传入了falsedescription会被排除。

不传参数即使用 fragment 为局部声明的$showDetailedResults的默认值。这为"大多数场景一个默认行为,少数场景定制覆盖"的组件设计提供了非常自然的表达方式。

从编译器测试看参数内联的完整流程

Relay 编译器的ApplyFragmentArgumentsTransform变换(见 apply_fragment_arguments.rs)是这套机制的核心:它将一组包含"带参数的 fragment 及 fragment spread"的文档,转换为所有参数都已内联的等价文档,文档头部注释将其精辟概括为"对函数进行静态柯里化(static currying)"。其主要行为包括:

  • 带参数的 fragment spread 被替换为引用一份"已内联"版本的 fragment;
  • @argumentDefinitions的 fragment 会针对每组唯一参数克隆一次,名称变为"原名 + hash",所有嵌套的变量引用被替换为参数对应的值;
  • 字段与指令参数中的变量被替换为其上下文中的值;
  • 字面量@include/@skip条件会被静态求值:条件恒真时消除条件节点并把选择集内联到父级,恒假时直接删除节点。

仓库中的测试 fixture inlines-fragment-arguments.graphql 与其 expected 输出 直观展示了这一过程:输入中同一份Profilefragment 以不同@arguments被展开,输出中被克隆为Profile_4FmGHPProfile_4CNNX6两个不同片段:

query TestQuery( $id: ID! $pictureSize: [Int] = [128] $includeFriends: Boolean = true ) { node(id: $id) { id ...Profile @arguments(pictureSize: $pictureSize, includeFriends: $includeFriends) } } fragment Profile on User @argumentDefinitions( pictureSize: {type: "[Int]"} includeFriends: {type: "Boolean!", defaultValue: false} ) { # ... }

变换后的输出(节选):

query TestQuery(...) { node(id: $id) { id ...Profile_4FmGHP } } fragment Profile_4CNNX6 on User { id name profilePicture(size: $pictureSize) { uri } } fragment Profile_4FmGHP on User { id name profilePicture(size: $pictureSize) { uri } friends(first: 10) @include(if: $includeFriends) { edges { node { ...Profile_4CNNX6 } } } }

注意includeFriends的默认值false是逐"克隆实例"生效的:Profile_4FmGHP保持@include(if: $includeFriends)的运行时条件,而递归引用的Profile_4CNNX6则对应另一组参数。这也解释了"每个唯一参数组合生成唯一 fragment"的命名策略——同名 fragment 因参数不同而拥有不同语义,必须用 hash 后缀区分。

在运行时访问查询变量

如果你希望在运行时访问**查询根(query root)**处设置的变量,官方推荐的做法是在组件树中通过 props(或你应用自有的 context)把变量逐层向下传递。

需要明确的是:Relay 目前不会向某个特定 fragment 暴露其"解析后"(即应用了 argument definitions 之后的)变量值,而且你极少会真的需要这样做。从运行时源码看,RelayConcreteVariables.js 提供了getOperationVariables(将查询级变量与variables合并得到实际请求变量)与getLocalVariables(为 fragment 生成局部变量对象)等工具函数,它们是 Relay 内部在读取数据时完成变量解析的入口,而非面向业务组件暴露的 API。业务侧若需要"当前查询用了哪些变量",最稳妥、最符合 Relay 数据流的方式仍然是把它们作为 props 显式传入相关组件。

小结:三种变量用法的取舍

用法声明位置作用域典型场景代价
查询变量查询参数列表查询全局,所有后代 fragment 可见查询根级输入,如$id包含 fragment 的查询都必须声明其用到的所有变量
fragment 引用全局变量fragment 内部直接使用$var依赖外层查询声明fragment 共享查询级输入修改 fragment 可能牵连多个查询定义
@argumentDefinitions+@argumentsfragment 内声明、spread 处传参fragment 局部,可带默认值组件级可定制、可复用片段需在 spread 处显式传参(除非有默认值)

设计建议:fragment 应优先考虑把真正属于"该数据视图自身"的可变输入声明为局部参数并给出合理默认值;只有当某个值确实来源于查询根、且被整棵子树共享时,才使用全局查询变量。这样既能最大化 fragment 的复用性,也能让 Relay 编译器在构建期替你兜住"变量声明缺失"这类低级错误。

  • 前端
  • 开发工具

【免费下载链接】relay

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

点击查看免费下载

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

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

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

立即咨询