Relay 编译器中 docblock-shared:Relay Resolvers 的 SDL 指令与 Docblock 标签共享契约解析
2026/9/21 0:10:04 网站建设 项目流程

导读

在 Relay 的 Rust 编译器中,docblock-shared是一个规模极小却处于核心位置的共享 crate:它集中定义了 Relay Resolvers 在 SDL(Schema Definition Language)指令与 JS Docblock 注解之间传递元数据时所需的全部"常量名"——包括指令名、指令参数名、docblock 标签名与特殊字段名。本文以 docblock-shared/README.md 为主线,结合其 lib.rs 源码与下游消费方实现,讲解这些名称为何必须以共享 crate 的形式存在、它们的真实取值、在 schema 扩展中的落地方式,以及它们如何支撑从 JS 代码提取 Resolver 到生成可感知 Resolver 的编译产物的完整链路。读完本文,你将能准确理解 Relay Resolver 编译管线的"隐式契约"层,并能在阅读relay-docblockrelay-transformsrelay-typegen等 crate 源码时快速定位这些名称的定义源头。

背景:Relay Resolvers 从 Docblock 中诞生

Relay Resolvers 允许开发者在 JavaScript/TypeScript 代码中直接声明 GraphQL 字段与类型,从而扩展 schema。其提取机制是:编译器从 JS 代码中查找Docblock 注解@RelayResolver@relayType@relayField等),并从这些注解中提取出 schema 定义。正如 docblock-shared/README.md 所述,这些 schema 定义还需要携带额外元数据,才能让 Relay 知道:

  • 哪些类型/字段是由 Resolver 支撑的(而不是服务端字段);
  • 如何调用对应的 Resolver 函数(函数从哪个模块、以什么名字导出);
  • Resolver 使用了哪些特性(如@live@weak@outputType@returnFragment等)。

这些元数据被挂在生成 schema 定义之上的SDL 指令(directive)中传递,而指令的名称与参数名必须被"提取 Resolver 的一方"与"消费 Resolver 生成产物的一方"共同遵守,这就产生了共享常量的需求。

为什么需要一个"共享 crate":指令即隐式契约

在 Relay 编译器的 crate 拓扑中,从 JS 提取 Resolver 的代码位于 relay-docblock 与 relay-schema-generation,而消费 Resolver 元数据生成产物的代码分散在 relay-transforms、relay-typegen、relay-codegen 等 crate 中。

这些 crate 之间不存在直接依赖关系,却需要就"指令叫什么、参数叫什么"达成完全一致。若各自硬编码字符串字面量,任何一个拼写差异都会导致编译管线静默失效。因此 docblock-shared 将这些名称抽取出来,形成文档中所说的"SDL 指令作为各部分之间的隐式契约"。这一设计思想与 docblock-syntax(负责把 docblock 文本解析为 AST)形成互补:docblock-syntax 解决"怎么写",docblock-shared 解决"叫什么"。

从依赖关系也可以印证这一点:docblock-shared 的 Cargo.toml 仅依赖common(类型安全的DirectiveName/ArgumentName/ScalarName)、intern(字符串驻留)、md-5(用于 source hash)与serde/hex,不依赖任何业务 crate,因此可以被任意下游 crate 安全引用而不会产生循环依赖。

关键设计:SDL 永不落盘,只在内存中构建 AST

README 中有一个容易被忽视但非常重要的说明:Resolver 的 SDL 从未以.graphql文件形式存在过。相反,负责从 JS 提取 Resolver 的代码分析会在内存中构建该 SDL 的 AST,再传递给 Relay 其余部分。

这样做的收益是:可以让 SDL 中的位置信息(span 与 location key)直接指向原始的 JS 代码——名称、类型等元素是从哪段 JS 源码中推导出来的,编译器就可以精确回溯。这对错误诊断、代码跳转(LSP)、增量编译都至关重要。README 将这种"把 JS 编译产物塑造成目标语言 AST"的模式总结为compile-to-AST模式。

在仓库中可以找到这条链路的实际载体:

  • docblock-syntax/src/lib.rs 中的parse_docblock/parse_docblock_with_offset把 docblock 内容解析为DocblockAST,后者在其 ast.rs 中携带locationsource_hash
  • relay-schema-generation/src/lib.rs 通过contains_resolver_tag过滤包含 Resolver 标签的注释,再据此在内存中生成对应的 schema 定义与指令节点。

也就是说,"SDL 以指令形式携带元数据"与"AST 在内存中构造、位置信息指向 JS 源码"两条原则共同定义了 Relay Resolver 编译管线的数据流。

docblock-shared 的完整常量清单

docblock-shared的核心资产全部集中在 src/lib.rs。下面按类别完整梳理(以下取值均来自源码,可通过链接逐条核对)。

1. Resolver 主指令

常量取值作用
RELAY_RESOLVER_DIRECTIVE_NAMErelay_resolver附加在 Resolver 字段与类型上,标记其由 Relay Resolver 支撑;其参数用于描述 Resolver 使用的特性(如@live@weak)。

该指令在 Relay 的 schema 扩展文件 relay-extensions.graphql 中有正式声明,供build_ir之前的 schema 校验通过:

directive @relay_resolver( fragment_name: String! import_path: String! live: Boolean ) on FIELD_DEFINITION

注意:这个文件中的声明只是指令"最简可用形态";docblock-shared 中定义的全部参数(见下表)会在编译过程中由提取方按需填充。

2.@relay_resolver指令的参数名

常量取值语义
FRAGMENT_KEY_ARGUMENT_NAMEfragment_name记录该 Resolver 的@rootFragment(如有)的名称。
TYPE_CONFIRMED_ARGUMENT_NAMEtype_confirmed标记提取机制已验证该 Resolver 的 Flow/TypeScript 类型与 GraphQL 类型一致。
RESOLVER_PROPERTY_LOOKUP_NAMEproperty_lookup_name标记该 Resolver 只是对底层模型的属性读取,需要生成对应的属性查找代码。
LIVE_ARGUMENT_NAMElive对应 docblock 的@live标签,表示字段/强模型 Resolver 会随时间变化。
IMPORT_NAME_ARGUMENT_NAMEimport_name类型/函数被导出的名字,供 codegen/typegen 生成 import 语句。
IMPORT_PATH_ARGUMENT_NAMEimport_path类型/函数被导出的模块路径。
HAS_OUTPUT_TYPE_ARGUMENT_NAMEhas_output_type标记该 Resolver 是否带有@outputTypedocblock 标签。
RETURN_FRAGMENT_ARGUMENT_NAMEreturn_fragment记录来自@returnFragment标签的 fragment 名。
MAY_WATERFALL_ARGUMENT_NAMEmay_waterfall标记 shadow Resolver 可能返回指向不同服务端对象的指针,所有消费方必须用@waterfall声明可能的 refetch。
INJECT_FRAGMENT_DATA_ARGUMENT_NAMEinject_fragment_data指定 Resolver 期望被注入的字段(如父对象id或模型的__relay_model_instance)。
GENERATED_FRAGMENT_ARGUMENT_NAMEgenerated_fragment记录编译器为注入 id/model 实例而生成的 fragment 名称。

3. 内部指令(模型、弱对象、shadow)

常量取值语义
RELAY_RESOLVER_MODEL_DIRECTIVE_NAME__RelayResolverModel标记"模型类型"(由 JS 模型值支撑的类型)。
RELAY_RESOLVER_MODEL_GENERATED_ID_FIELD_DIRECTIVE_NAME__RelayResolverModelGeneratedIDField标记模型类型上生成的 ID 字段。
RELAY_RESOLVER_WEAK_OBJECT_DIRECTIVE__RelayWeakObject标记"弱"模型类型(无稳定身份)。
SHADOW_RETURN_DIRECTIVE_NAME__relay_shadow_returnshadow Resolver 的@returnFragment占位 spread 在build_ir之前被转换成的内部指令;已在 relay-extensions.graphql 声明以便通过 schema 校验。
SHADOW_RETURN_FRAGMENT_ARGUMENT_NAMEfragment上述指令中携带原始@returnFragment占位名。

4. 特殊字段与标量

常量取值语义
RELAY_RESOLVER_MODEL_INSTANCE_FIELD__relay_model_instance模型实例在 Relay 运行时中以父类型上的隐藏字段建模,使用较长名字避免与产品代码的__self/__instance冲突。
KEY_RESOLVER_ID_FIELDid强模型类型的 ID 字段名(仅限 Resolver 使用;服务端类型的 id 字段由配置指定,不能硬编码)。
RESOLVER_VALUE_SCALAR_NAMERelayResolverValue自定义标量,Resolvers 可作为返回类型;此时字段的 Flow/TypeScript 类型从 Resolver 函数返回值推导,允许返回任意(不可序列化)JS 值作为逃生舱。该标量在 relay-extensions.graphql 中声明。
ARGUMENT_DEFINITIONSargumentDefinitionsRelay 实现 fragment 参数的指令名(此处复制是因为@rootFragment可能需要检查参数)。
ARGUMENT_TYPE/DEFAULT_VALUE/PROVIDER_ARG_NAMEtype/defaultValue/provider上述@argumentDefinitions使用的参数名。
EMPTY_STRING""空字符串的驻留常量。

5. Docblock 标签名(tags)

README 明确指出"本 crate 也暴露了 Relay Resolvers 当前使用的 docblock 标签名"。完整清单如下:

常量取值语义
RELAY_RESOLVER_FIELD@RelayResolver标记字段为 Resolver。
RELAY_TYPE_FIELD@relayType定义 Relay Resolver 类型。
RELAY_FIELD_FIELD@relayField定义 Relay Resolver 字段。
DEPRECATED_FIELD@deprecated标记 Resolver 已弃用(会为字段附加等价@deprecated指令;GraphQL 规范不允许标记类型弃用)。
LIVE_FIELD@live标记 Resolver 是"live"的,返回可订阅的值。
SEMANTIC_NON_NULL_FIELD@semanticNonNull用于标记类型为非空的 Resolver(Relay 对语义可空性的实验支持)。
ROOT_FRAGMENT_FIELD@rootFragment标记 Resolver 从某 fragment 读取数据,并给出 fragment 名。
RETURN_FRAGMENT_FIELD@returnFragment标记 shadow Resolver 返回符合某 fragment 形状的数据。
MAY_WATERFALL_FIELD@mayWaterfall配合@returnFragment,声明可能返回不同服务端对象;缺少时消费方的@waterfall会被拒绝。
WEAK_FIELD@weak定义"弱"类型,其后的类型导出将作为该类型底层模型的 Flow/TypeScript 类型。

6. Source hash 相关

常量取值语义
RELAY_RESOLVER_SOURCE_HASHresolver_source_hash@relay_resolver并行的指令,携带生成该 Resolver 的 docblock 的哈希,用于保证 docblock 变更会使编译器状态失效。
RELAY_RESOLVER_SOURCE_HASH_VALUEvalue上述指令中承载哈希值的参数名。

contains_resolver_tag:一次遍历快速判定 Resolver Docblock

src/lib.rs 还提供了一个实用的纯函数contains_resolver_tag:单次遍历字符串,检查是否包含@RelayResolver@relayType@relayField三个标签中的任意一个。实现上按字节扫描@字符后做前缀匹配,避免多次str::contains各自全量扫描的开销。

该函数在管线中的应用:在 relay-schema-generation/src/lib.rs,提取方用它过滤候选注释,只对包含 Resolver 标签的注释进行 schema 生成;在 extract-graphql/src/lib.rs,它也被用来判断一个文件是否需要按 Resolver 模式处理。可见这一"廉价预筛"在多个入口被复用,正是共享 crate 价值的又一体现。

ResolverSourceHash:docblock 内容 → MD5 哈希

src/resolver_source_hash.rs 定义了ResolverSourceHash结构体,其内部是驻留后的 MD5 十六进制字符串:

  • ResolverSourceHash::new(source):对 docblock 原文计算 MD5 并驻留;
  • from_raw/value:用于反序列化与读取。

它被 docblock-syntax/src/ast.rs 嵌入DocblockAST,随每次 docblock 解析生成。这样,只要 docblock 文本发生任何改动,哈希就会变化,进而通过resolver_source_hash指令写入生成的 schema 定义,使编译器能够检测到"由 Resolver 派生的 schema 已过期"并触发重建。

这一机制与 Relay 编译器对普通 GraphQL 文件的增量失效策略(见 build_ir.rs:对每个可执行定义打印其源码并计算 MD5 得到SourceHashes)思路一脉相承:任何派生产物都必须锚定其来源文本的哈希,才能可靠地驱动增量构建。

消费方全景:谁在使用这些共享名称

通过全仓库检索docblock_shared::引用,可以看到这些常量被下列 crate 广泛消费,形成一张完整的契约网:

  • 提取侧:relay-docblock、relay-schema-generation(含find_property_lookup_resolvers.rserrors.rs)、docblock-syntax;
  • 转换侧:relay-transforms、shadow_transform.rsfragment_dependencies.rsclient_edges.rsrelay_resolvers_abstract_types.rs以及generate_relay_resolvers_*系列、match_/match_transform.rs、若干校验模块;
  • 生成侧:relay-typegen、relay-codegen、relay-compiler;
  • 工具侧:relay-lsp、relay-schema。

这些 crate 各自依赖 docblock-shared 中的同一组常量来读写指令,从而在"提取方"与"生成方"之间维持一致性。例如 relay-docblock/src/docblock_ir.rs 直接引入ARGUMENT_DEFINITIONSARGUMENT_TYPEDEFAULT_VALUEPROVIDER_ARG_NAMEKEY_RESOLVER_ID_FIELDResolverSourceHash,用于构造 Resolver 的 IR;而 generate_artifacts.rs 则把source_hash写入产物元数据,完成从"docblock → schema → IR → artifact"的闭环。

实战视角:如何定位与阅读相关代码

如果你正在阅读 Relay 编译器源码并遇到陌生的@relay_resolver@rootFragment__relay_model_instance等名称,阅读路径建议如下:

  1. 先到 docblock-shared/src/lib.rs 查找该名称的常量定义,获得权威语义注释;
  2. 若涉及 schema 合法性问题,到 relay-extensions.graphql 查看指令/标量在 schema 中的声明位置与用途说明(如@__relay_shadow_returnRelayResolverValue@waterfall@RelayOutputType);
  3. 若关心 docblock 解析细节,阅读 docblock-syntax/src/lib.rs 的parse_docblock与 ast.rs 的DocblockAST::find_field
  4. 若关心哈希失效,对照 resolver_source_hash.rs 与 build_ir.rs 的SourceHashes

由于本仓库是只读镜像,上述文件均可直接在compiler/crates/目录下打开查阅,无需任何安装或构建步骤。

总结

docblock-shared是 Relay Rust 编译器中一个"以命名即契约"的典型范例:它不实现任何复杂算法(仅提供一个预筛函数与一个 MD5 封装),却通过集中定义 SDL 指令名、参数名、docblock 标签名与特殊字段名,把分散在十余个 crate 中的 Resolver 提取与生成逻辑锚定在同一套词汇表上。理解这份共享清单,就等于拿到了阅读整个 Relay Resolvers 编译管线的"索引表"——无论是排查 schema 生成问题、理解增量失效机制,还是扩展新的 Resolver 特性,都能从这里找到准确、权威的名称语义。

  • 前端
  • 开发工具

【免费下载链接】relay

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

点击查看免费下载

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

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

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

立即咨询