Relay 19 GraphQL 指令全指南:掌握 `@arguments`、`@connection`、`@required` 等内置指令的编译期与运行时语义
Relay 19 GraphQL 指令全指南掌握arguments、connection、required等内置指令的编译期与运行时语义【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relayRelay 通过在 GraphQL 文档上附加一组以开头的客户端指令client directives为查询、片段、字段注入额外的编译期语义让 Relay 编译器生成正确的运行时产物normalization AST、Flow/TypeScript 类型、分页/重取查询等。本指南以本文档为核心逐条讲解每个指令的语法、参数、行为与适用场景并结合仓库中relay-transforms等 crate 的源码说明其底层实现读完你就能在 Relay 19 项目中正确选用与组合这些指令。关键前提这些指令只出现在你的应用代码里编译后会从发送给 GraphQL 服务器的请求中移除。Relay 编译器会保留服务器支持的指令如标准include、skip它们仍会随请求发往服务器且不会改变生成的运行时产物。1.arguments与argumentDefinitions让片段支持参数片段默认是自包含的无法接收外部变量。Relay 提供一对指令为片段引入片段级参数fragment argumentsargumentDefinitions定义片段可接收的参数类型、默认值arguments在父级 spread 片段时把值传入。1.1 定义片段参数fragment TodoList_list on TodoList argumentDefinitions( count: {type: Int, defaultValue: 10} # 可选参数带默认值 userID: {type: ID} # 必选参数必须由调用方传入 ) { title todoItems(userID: $userID, first: $count) { # 在片段内部片段参数以 $ 变量形式使用 ...TodoItem_item } }要点每个参数是一个{type: ..., defaultValue: ...}对象defaultValue省略即为必选参数片段内部通过$参数名引用这些参数用于字段参数或include/skip条件片段参数的取值在编译期被固化下文会看到其实现机制。1.2 传入片段参数query TodoListQuery($userID: ID) { ...TodoList_list arguments(count: $count, userID: $userID) # Pass arguments here }arguments只能传给argumentDefinitions声明过的参数传入值可以是查询根变量如$userID也可以是字面量。1.3 底层实现编译期的静态柯里化从源码结构看片段参数并非运行时特性而是由ApplyFragmentArgumentsTransform在编译期完成的静态柯里化。apply_fragment_arguments.rs 的模块注释明确写道带参数的片段 spread 会被替换为对该片段应用后版本的引用声明了参数的片段会按每组唯一的参数组合克隆一次名称改为「原片段名 参数哈希」片段内部所有变量引用会被替换为对应参数的实参值字段与指令参数中的变量也会按上下文替换。同时该 transform 还会处理Condition节点字面量恒真的条件被展开内联恒假的条件被删除变空的选择集随之移除见 apply_fragment_arguments.rs。片段参数的类型检查与报错示例可参见 graphql-ir 的解析测试。2. Provided Variables把运行时常量注入片段Provided Variable 是一种特殊的片段变量其值由指定的 provider 函数在运行时提供适用于设备属性、用户实验开关等运行时常量避免把它们作为查询变量层层下传。2.1 声明与使用向argumentDefinitions添加带provider的参数即可声明 provided variablefragment TodoItem_item on TodoList argumentDefinitions( include_timestamp: { type: Boolean! provider: Todo_ShouldIncludeTimestamp.relayprovider } ) { timestamp include(if: $include_timestamp) text }配套的 provider 模块文件名必须为[JSModule].relayprovider.js导出get()// Todo_ShouldIncludeTimestamp.relayprovider.js export default { get(): boolean { // must always return true or false for a given run return check(todo_should_include_timestamp); }, };约束get()在同一运行周期内必须每次都返回相同的值尽管 provided variable 声明在argumentDefinitions里父片段不能通过arguments向它传值——编译期会报Passing a value to ... (a provided variable) through arguments is not supported.见 apply_fragment_arguments.rs一个参数定义不能同时指定provider和defaultValue不稳定Unstable随时可能调整。2.2 底层实现重命名为__relay_internal__pv__[JsModule]provided_variable_fragment_transform会把 provided variable 从片段局部参数中剥离、提升为操作的根变量并重命名为__relay_internal__pv__[JsModule]调试含 provided variable 的查询时你会看到该名字。util.rs 中的format_provided_variable_name用正则校验模块名仅允许[A-Za-z0-9_]非法字符会被剥离后拼接成__relay_internal__pv__module。该 transform 还会做一致性校验见 provided_variable_fragment_transform.rs同一个去规范化后的模块名在不同地方使用必须声明相同的类型否则报All provided variables using module {module} must declare the same type.模块名去掉非字母数字字符后若无法区分报Modules {module1} and {module2} used by provided variables have indistinguishable names.。整个编译管线中apply_fragment_arguments依赖provided_variable_fragment_transform先行执行见 provided_variable_fragment_transform.rs随后由apply_fragment_arguments把收集到的 provided variables 追加到根操作的变量定义中见 apply_fragment_arguments.rs。3.catch声明字段级错误处理方式catch可加在字段、片段、查询、变更mutation、带别名的内联片段上声明字段级错误field errors在运行时的处理方式。它让错误以数据形态暴露而不是静默变成null或抛出异常。3.1 两种错误呈现方式to参数catch接受可选的to参数RESULT默认错误字段返回{ ok: true, value: T } | { ok: false, errors: [error] }便于做字段粒度的显式错误处理NULL遇到错误时该字段值替换为null。源码中CatchTo枚举Null/Result与to参数解析位于 catch_directive.rs未知取值会 panic 提示「UseNULLorRESULT(default) instead.」。3.2 错误冒泡行为错误发生在catch标注的字段本身错误就挂在那个字段上若catch在某个字段的祖先上错误会上浮到该祖先字段。query MyQuery { viewer { name catch age } }若name出错响应数据为{ viewer: { name: { ok: false, errors: [{path: [viewer, name]}] } age: 39 } }而若catch放在祖先viewer上query MyQuery { viewer catch { name age } }错误会整体冒泡到viewer{ viewer: { ok: false, errors: [{ path: [viewer, name] } ] } }3.3catch能捕获什么Payload 字段错误服务器执行某字段 resolver 抛出的异常Relay 把原本的null errors 数组就地呈现在数据里required(action: THROW)在catch祖先内不再抛异常而是像普通错误一样冒泡进catch的结果响应中缺失数据missing data字段应有值但缺失时同样被catch捕获。3.4 与throwOnFieldError的交互throwOnFieldError开启时字段错误会导致 JS 异常若该字段被catch覆盖Relay 则不抛异常改为把错误放进数据对象。注意二者都只处理指令所在查询/片段/变更内的字段错误不处理 spread 片段内部的字段错误。完整实战讲解见 catch 指南。4.connection(key: String!, filters: [String])分页连接使用usePaginationFragment时Relay 要求连接字段connection field标注connection以便把连接数据按 key 归入统一的存储区并支持增量分页。fragment FriendsList on User refetchable(queryName: FriendsListFetchQuery) { friends(after: $cursor, first: $count) connection(key: FriendsList_friends, filters: [orderBy]) { edges { node { ... } } } }key: String!唯一标识规范是「片段名_字段名」不同的连接必须使用不同 keyfilters: [String]声明连接的非分页参数如排序、过滤条件作为连接存储 key 的一部分。从源码结构看connection的解析与默认 filters 逻辑在 connection_util.rs未显式指定filters时默认把所有不属于连接规范参数after/before/first/last/find/surrounds的参数都当作 filters。连接规范参数列表定义于 connection_constants.rs。编译时connection会被转换为内部的 handle field 指令见 transform_connections.rs运行时由relay-runtime/handlers/connection下的 handler 处理增删改。详细用法与示例见 渲染连接Rendering Connections 与usePaginationFragment。5.refetchable(queryName: String!, directives: [String], preferFetchable: Boolean)自动生成重取查询useRefetchableFragment和usePaginationFragment都要求片段标注refetchable编译期会自动生成一个名为queryName的查询并生成对应的 Flow 类型可从生成文件queryName.graphql.js导入。适用约束refetchable只能加在可重取的片段上即类型条件为Viewer、Query或实现了Node接口有id字段的类型。graphql fragment FriendsListComponent_user on User refetchable( queryName: FriendsListFetchQuery directives: [relay_test_operation] ) { ... } ;参数说明queryName: String!自动生成的查询名directives: [String]可选往自动生成的查询上附加的指令列表例如为组件测试添加relay_test_operationpreferFetchable: Boolean可选指示编译器对实现了Node接口的类型优先生成fetch_MyType(): MyType形式的查询。对已采用服务端strongfetchable注解的 schema 尤其有用——可直接取回具体对象无需先经Node接口再做类型收窄。底层实现多种查询生成器编译器解析refetchable的三个参数queryName必须是字符串字面量、directives必须是字符串字面量列表、preferFetchable必须是常量布尔值见 refetchable_directive.rs。随后依据片段类型条件与 schema 选择生成策略node_query_generator.rs基于Node接口 / 实现Node的对象 / 成员全部实现Node的接口与联合类型生成node(id: $id)查询fetchable_query_generator.rs当 schema 类型带fetchable注解时preferFetchable触发该路径生成fetch_MyType(id: $id)查询并自动补充 id 字段必要时以Node内联片段形式补id见该文件的enforce_selections_with_id_field。更多细节见useRefetchableFragment与usePaginationFragment。6.relay(plural: Boolean)声明复数片段为useFragment定义片段时relay(plural: true)表示该 hook 期望的 prop 是元素列表而非单个对象。spread 它的查询/父片段必须放在列表字段内即 GraphQL list 字段中。// Plural fragment definition graphql fragment TodoItems_items on TodoItem relay(plural: true) { id text } ; // Plural fragment usage: note the parent type is a list of items (TodoItem[]) fragment TodoApp_app on App { items { // parent type is a list here ...TodoItem_items } }底层实现relay指令的参数mask、plural在 relay_directive.rs 中定义指令位置是FRAGMENT_DEFINITION | FRAGMENT_SPREAD。解析逻辑relay_directive.rs只接受mask与plural两个布尔参数未知参数或非布尔常量会直接 panic编译器在前置 validation 中已保证合法性。plural: true会改变useFragment生成的 prop 类型ReadonlyArray...同时影响alias等指令的校验路径见 fragment_alias_directive.rs。7.required声明空值null处理策略required加在查询字段上声明运行时空值如何处理。可以理解为如果该字段是 null它的父字段就无效也应视为 null。被标注字段在生成的类型中将变为非空non-nullable。query MyQuery { viewer { name required(action: LOG) age } }若name为 nullRelay 会返回{ viewer: null }。7.1action参数的四种取值action是必填参数action语义行为NONE预期可空该字段预期有时为 nullnull 值冒泡到父字段父字段变为 null组件检查父字段即可LOG可恢复不该为 null但 null 时组件仍可渲染触发 field logger 的missing_required_field.log事件null 冒泡到父字段THROW不可恢复组件离开该字段无法渲染读取时抛异常错误信息含 owner 与字段路径组件需置于 React error boundary 内仅能用于 schema 中非空字段或semanticNonNull字段若开启对应编译特性开关DANGEROUSLY_THROW_ON_SEMANTICALLY_NULLABLE_FIELD绕过校验的 THROW与 THROW 相同但允许用在可空字段上仅作为迁移路径强烈不建议新代码使用仅在开启disallow_required_action_throw_on_semantically_nullable_fields编译特性开关时需要动作常量的定义见 required_directive.rs。7.2 局部性Localityrequired的生效范围局限于其所在的片段因此可以在不同组件里对同一字段采取不同策略。但同一片段内同一字段的所有引用必须一致——典型冲突场景是内联片段里对同一字段有的加required有的不加编译器会报All references to a field must have matching required declarations.。7.3 链式使用Chainingrequired可链式串联一次判空即可安全访问深层字段const user useFragment(graphql fragment MyUser on User { name required(action: LOG) profile_picture required(action: LOG) { url required(action: LOG) } }, key); if(user null) { return null; } return img src{user.profile_picture.url} alt{user.name} /注意子字段required会令片段根/父字段也变成可空类型类型系统会体现。链式时子节点的 action 不能比父节点更温和否则编译报错A required field may not have anactionless severe than that of its required parent.。7.4 与connection的已知限制connection会自动插入若干额外字段这些字段不会带上required因此在 Connection 类型上使用required可能导致不一致错误。node字段及其直接子字段不能加requiredtitle等更深层字段可以fragment FriendsList on User refetchable(queryName: FriendsListQuery) { friends(after: $cursor, first: $count) connection(key: FriendsList_friends) { edges { node { # 不能加 required job { # 不能加 required title required(action: LOG) # 可以 } } } } }更多问答见 required 指南。8.throwOnFieldError读取时遇字段错误即抛出throwOnFieldError加在查询与片段上当读取该查询/片段遇到任何字段错误或因 图关系变化导致的缺失数据时Relay 运行时直接抛异常。fragment MyFragment on User throwOnFieldError { id name }附加收益该指令开启后schema 中带semanticNonNull的字段会生成非空类型。因为错误会直接抛出应用永远不会收到 null 值原先很多required就变得多余可借助 remove-unnecessary-required-directives codemod 清理。若想在throwOnFieldError的片段/查询里对某个字段做本地化错误处理可配合catch捕获。使用throwOnFieldError的组件务必置于合适的 React error boundaries 之下。完整讲解见 throwOnFieldError 指南。9.semanticNonNullschema 端与语义空值semanticNonNull是加在服务端 schema 字段上的指令表示该字段在语义上非空resolver 正常情况不会返回 null但客户端仍要准备好处理错误directive semanticNonNull(levels: [Int] [0]) on FIELD_DEFINITION type User { name: String semanticNonNull }Relay 会扫描 schema 中的semanticNonNull当查询/片段启用了客户端错误处理throwOnFieldError时为这些字段生成非空的 Flow/TypeScript 类型见 semantic-nullability.md。这是 Relay 对 GraphQL「语义空值Semantic Nullability」实验性规范提案的落地支持——把错误处理与可空性解耦既保持字段级错误的弹性又把 resolver 的真实语义暴露给客户端。更多背景见 Semantic Nullability 指南。10.alias给片段 spread 与内联片段起别名alias允许给命名片段 spread 或内联片段起一个别名类似字段别名用于条件包含片段时检查它是否被取回、或把数据分组。片段 spread 的别名默认取片段名内联片段的别名默认取类型名想自定义名字、或内联片段没有类型条件时用as参数显式指定。fragment MyFragment on User { ... on User alias(as: myGreatAlias) { name } }典型价值在抽象类型如Node上 spread 一个具体类型如Viewer的片段时alias让片段 key 作为独立的可空属性暴露先判空再安全使用skip/include条件片段也可通过别名是否为 null 判断是否被取回。编译器通过 fragment_alias_directive.rs 解析指令并把别名信息写入运行时产物此外还内置了dangerously_unaliased_fixme用于迁移期抑制强制校验相关校验与强制开关enforce_fragment_alias_where_ambiguous的讨论见 alias 指南。11.inline在渲染阶段之外读取数据Relay 的 hooks API 只允许在渲染阶段从 store 读数据。若需要在渲染之外或 React 之外读取可用inline标注片段再用readInlineData读取。import {graphql, readInlineData} from react-relay; // non-React function called from 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, }); }所有使用该函数的组件都要 spread 这个inline片段确保所需数据都被加载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片段由InlineDataFragmentsTransform处理inline_data_fragment.rs生成可在任意位置读取的内联数据节点readInlineData则从 runtime 侧完成数据读取。12.relay(mask: false)关闭数据遮蔽不推荐relay(mask: false)用于关闭数据遮蔽data masking被标注片段的数据会直接暴露给父级而非为不同容器各自遮蔽。官方不推荐使用优先考虑inline。应用到片段定义上时它会让生成的 Flow 类型不再是非精确对象exact object也不再包含内部标记字段便于处理单组件内的嵌套/递归数据graphql fragment Component_internUser on InternUser relay(mask: false) { id name } ;此后无论在哪里 spread...Component_internUseruserprop 都会直接包含id、name数据。警告跨多个容器共享单一片段通常被视为反模式anti-pattern滥用relay(mask: false)可能导致应用过度取数over-fetching。13.waterfall标记 Relay Resolvers 的服务端类型边在 Relay Resolvers 中可以创建指向服务端类型的客户端定义边。读取这类边字段时Relay 会惰性拉取该边对应的服务端数据从而产生第二次网络请求。为了让编辑器和代码评审都能注意到这个取舍编译器要求所有对这类字段的读取必须标注waterfallfragment EditPost on DraftPost { author waterfall { name } }waterfall只是编译器层面的强制标注不改运行时语义——它的存在提醒你这里会多一次级联网络往返请慎重。详见 Return Types 中关于服务端类型Server Types的部分。14. 总结指令速查表指令位置核心参数用途argumentDefinitions片段定义{type, defaultValue}列表声明片段参数arguments片段 spread具名实参向片段传参catch字段/片段/查询/变更/带别名内联片段to: RESULT \| NULL声明字段级错误处理connection连接字段key: String!,filters: [String]连接分页存储refetchable片段定义queryName: String!,directives: [String],preferFetchable: Boolean自动生成重取查询relay(plural)片段定义/片段 spreadplural: Boolean声明复数片段required字段action: NONE/LOG/THROW/DANGEROUSLY_THROW_ON_SEMANTICALLY_NULLABLE_FIELD声明空值处理throwOnFieldError查询/片段无读取遇字段错误即抛异常semanticNonNullschema 字段定义服务端levels: [Int]声明语义非空alias片段 spread/内联片段as: String给片段/内联片段起别名inline片段定义无渲染阶段外读取数据relay(mask: false)片段定义/片段 spreadmask: Boolean关闭数据遮蔽不推荐waterfallRelay Resolvers 服务端类型边字段无标记惰性取数的级联往返一句话记忆片段参数arguments/argumentDefinitions/provided variables决定数据如何被参数化错误与空值指令catch/required/throwOnFieldError/semanticNonNull决定出错与缺失时如何表现分页与重取指令connection/refetchable决定列表如何增量加载其余指令relay/alias/inline/waterfall服务于类型安全、可复用性与渲染边界。它们全部在编译期被 Relay 编译器消化并转化为运行时产物不随请求发往服务器而include/skip等服务端指令会被原样保留。【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考