- 后端
【免费下载链接】graffle
Simple GraphQL Client for JavaScript. Minimal. Extensible. Type Safe. Runs everywhere.
Graffle 的 Document Builder 扩展提供了一组根级构建器(Root Level Builders)——通过Graffle命名空间直接解构出的query与mutation,让你不必编写完整 GraphQL 文档字符串,也不必创建客户端实例,就能用 TypeScript 对象语法快速构建类型安全的 GraphQL 查询与变更文档。本文以仓库中的官方示例为主线,逐一讲解单字段查询、带参数与嵌套选择集的查询、$batch批量操作、变更操作以及变量自动推断,并结合生成器源码(MethodsRoot.ts、生成的 methods-root.ts 与 document.ts)说明其底层原理,最后给出“何时用根级构建器、何时用Graffle.gql()”的选型建议。读完本文,你将掌握用 Graffle 在不依赖完整文档包装器的情况下快速、精准地构造 GraphQL 文档的完整技能。
什么是根级构建器
根级构建器是 Graffle 生成代码中暴露的一组静态方法对象。在官方示例 document-builder_root-level-builders.ts 的开头,通过一行解构即可获得它们:
import { Graffle } from './graffle/_.js' const { mutation, query } = Graffle这里Graffle是生成模块的命名空间(__.ts 中导出了mutation、query),query和mutation分别对应 GraphQL Schema 中的Query根类型与Mutation根类型。它们由graffle/extensions/document-builder的createStaticRootType创建(见 document.ts),是“静态”构建器:
- 无需客户端实例:不依赖
Graffle.create(),也不需要配置 transport; - 直接产出文档:每个字段方法接收一个选择集对象,返回类型安全的 GraphQL 文档字符串(类型上表现为
GraphqlKit.Document.Typed.String); - 编译期校验:字段选择、参数类型、变量推断全部在 TypeScript 层完成。
在生成模块中,query/mutation的接口由生成器MethodsRoot负责产出(MethodsRoot.ts 生成BuilderMethodsRoot,再按config.methodsOrganization.logical生成QueryMethods/MutationMethods等接口),每个根字段对应接口上的一个方法。例如生成的 methods-root.ts 中QueryMethods接口即包含$batch、__typename、pokemons、pokemonByName、trainerByName、trainers等方法,MutationMethods包含addPokemon等方法。
准备工作:示例环境与导入说明
本文所有代码都来自仓库中的可运行示例。示例运行在examples/目录下,其 schema 来自 pokemon schema,生成代码输出到 examples/$/graffle。
官方示例开头有一段注释值得注意:由于网站使用 Vitepress+Twoslash,而 Twoslash 无法自动发现生成的 Graffle 模块,因此需要显式导入全局模块:
import './graffle/modules/global.js' // ---cut--- import { Graffle } from './graffle/_.js'在你的真实 TypeScript 项目中,全局模块通常会被自动包含(__.ts 中的注释说明了这一点),不需要这个显式导入;只有某些 tsconfig 配置下才需要保留。
一、单字段查询:最简文档
最简单的用法是查询单个根字段,选择集内用true表示“选择该标量字段”:
const doc1 = query.pokemons({ name: true, hp: true, })它等价于下面的 GraphQL 文档:
{ pokemons { name hp } }注意:query.pokemons与执行(发送请求)无关,它只负责构建文档。官方输出(document-builder_root-level-builders.output.txt)验证了这一点——根级构建器的字段方法返回的是文档字符串,而不是执行结果。这也解释了文档中对根方法接口的 JSDoc 描述:“All methods return Promises. Use.query.$batch(...)to select multiple fields at once.”(见 methods-root.ts)。
从类型层面看,query.pokemons的方法签名由生成器产出(MethodsRoot.ts),其选择集参数会被NoExcess约束为 schema 允许的字段($$SelectionSets.Query.pokemons),传入 schema 中不存在的字段会在编译期报错。
二、带参数与嵌套选择的查询
根级构建器的完整形态支持字段参数(通过$键)与嵌套对象选择:
const doc2 = query.pokemonByName({ $: { name: 'Pikachu' }, name: true, type: true, trainer: { name: true, class: true, }, })等价文档:
query ($name: String!) { pokemonByName(name: $name) { name type trainer { name class } } }这里有三个关键机制:
$键声明参数:$: { name: 'Pikachu' }中$是参数声明区,与 arguments 示例 中的写法一致(例如$: { filter: { name: { in: [...] } } });- 自动变量化:虽然你写的是字面量
'Pikachu',但生成器/文档构建器会自动把它提升为 GraphQL 变量$name: String!(非空 String),并把参数位置替换为name: $name; - 嵌套选择:
trainer是对象字段,需要继续提供子选择集,选中其标量字段name、class。
这种“字面量自动变量化”正是根级构建器的核心体验——你不必手工管理变量定义,Graffle 会在编译期推断出变量类型并生成正确的变量声明。
参数名冲突:自动去重
当同一文档中多个字段使用了同名参数时,Graffle 会自动重命名以避免冲突。示例doc4:
const doc4 = query.$batch({ pokemonByName: { $: { name: 'Charizard' }, name: true, type: true, attack: true, }, trainerByName: { $: { name: 'Ash' }, name: true, pokemon: { name: true, }, }, pokemons: { name: true, }, })其输出为:
query ($name: String!, $name_2: String!) { pokemonByName(name: $name) { name type attack } trainerByName(name: $name_2) { name pokemon { name } } pokemons { name } }可以看到,两个字段的参数都叫name,Graffle 将第二个自动重命名为$name_2并正确映射到trainerByName(name: $name_2)。这保证了多字段批量文档中变量名唯一、不会相互覆盖。
三、$batch:一次操作多个根字段
当需要在一个操作(一个 GraphQL 请求)中选择多个根字段时,使用query.$batch(...)或mutation.$batch(...):
const doc3 = query.$batch({ pokemons: { name: true, hp: true, }, trainers: { name: true, class: true, }, })等价文档:
{ pokemons { name hp } trainers { name class } }$batch的签名由生成器产出(MethodsRoot.ts):它接收一个“以根字段名为键、以该字段选择集为值”的对象,并将其合并进单个 Query/Mutation 操作。它的 JSDoc 说明(jsdoc.ts 中的getBatchMethodDoc)指出:“Use this method to request multiple fields in a single request for better performance.”——这正是批量请求的用途:减少网络往返。
$batch同样支持参数与嵌套:
const runner = query.$batch({ pokemonByName: { $: { name: 'Pikachu' }, name: true, type: true, }, trainers: { name: true, class: true, }, })此时整份文档中所有变量会被统一收集到同一个变量声明块中。
批量还是嵌套选择?
$batch与“嵌套选择”适用不同场景:如果你要查询的是同一对象类型下的多个字段(如pokemons里的name、hp),直接在单个字段的选择集里写即可;$batch用于横向合并多个不同的根字段(entrypoint),例如把pokemons和trainers放进同一个请求。这与 batch 示例(同时批量pokemonByName与trainerByName)的用法一致。
四、变更操作(Mutation)
根级构建器同样覆盖Mutation根类型。单字段变更:
const doc5 = mutation.addPokemon({ $: { name: 'Mew', $type: 'water', hp: 100, attack: 100, defense: 100, }, name: true, type: true, })等价文档:
mutation ($name: String!, $type: PokemonType!, $hp: Int, $attack: Int, $defense: Int) { addPokemon( name: $name type: $type hp: $hp attack: $attack defense: $defense ) { name type } }值得注意的细节:
$type键转义:因为$前缀在 Graffle 选择集中保留给“变量/参数声明”,而addPokemon恰好有一个参数叫type,因此这里用$type(在$声明区内部再写$type)表示“参数名为type的变量”。同理,addTrainer的class参数写作$class(见下方doc6)。这是 Graffle 对 GraphQL 关键字/保留词与 schema 参数名冲突的约定转义方式。- 变量类型来自 schema:
name: String!、type: PokemonType!(枚举)、hp/attack/defense: Int(可空)——这些都是由生成器从 schema 中推导出的变量类型,而非手写。
多字段变更使用mutation.$batch:
const doc6 = mutation.$batch({ addPokemon: { $: { name: 'Mewtwo', $type: 'electric', hp: 106, attack: 110, defense: 90, }, name: true, }, addTrainer: { $: { name: 'Blue', $class: 'leader', }, name: true, class: true, }, })等价文档:
mutation ($name: String!, $type: PokemonType!, $hp: Int, $attack: Int, $defense: Int, $name_2: String!, $class: String!) { addPokemon( name: $name type: $type hp: $hp attack: $attack defense: $defense ) { name } addTrainer(name: $name_2, class: $class) { name class } }这里同样发生了两件事:第二个name参数自动重命名为$name_2;addTrainer的class参数通过$class转义后正常生成class: $class。
五、变量推断与延迟执行(Deferred Execution)
根级构建器的字段方法返回什么,取决于选择集中是否含有“变量标记”:
- 无变量:方法直接返回文档字符串(示例中
doc1~doc6都是这种情况); - 有变量(使用
$标记 + 字面量):文档字符串本身就是变量化的(如query ($name: String!) {...}),可以配合DocumentRunner使用。
当通过客户端实例(Graffle.create().use(DocumentBuilder()))使用根级方法且选择集中出现变量标记时,Graffle 会自动切换到延迟执行(deferred execution):方法返回DocumentRunner<Variables, Result>,而非立即执行的 Promise(详见 DocumentBuilder 扩展文档)。DocumentRunner提供:
document: string—— 生成的 GraphQL 文档字符串,可调试、可交给其他 GraphQL 客户端;run(variables): Promise<Result>—— 用指定变量执行,变量与结果均完全类型安全。
const runner = query.pokemonByName({ $: { name: $.String$NonNull }, // 显式变量标记 name: true, hp: true, }) console.log(runner.document) // query($name: String!) { pokemonByName(name: $name) { name hp } } const pikachu = await runner.run({ name: 'Pikachu' }) const charizard = await runner.run({ name: 'Charizard' }) // 同一文档复用关于“何时自动执行、何时延迟执行”的取舍,DocumentBuilder 文档 给出的建议是:一次性查询且无需变量时用自动执行;需要复用查询、检查文档、或对接其他 GraphQL 工具时用延迟执行。Graffle 会根据你是否书写变量标记自动选择。
六、何时使用根级构建器,何时使用 Graffle.gql()
官方示例最后给出了一份明确的选型对照(这也是 root-level-builders.md 的核心结论):
使用query/mutation根级构建器(含$batch),当:
- 你只需要简单、聚焦的操作;
- 你想一次批量选择多个根字段;
- 你不需要复杂的操作命名(operation name);
- 你在 schema-less / 静态文档场景下工作。
使用Graffle.gql()当:
- 你需要命名操作(named operations);
- 你想在一个文档中编写多个操作(operation);
- 你需要对文档结构有完全的控制;
- 你正在处理变量与复杂逻辑。
两者的差异在仓库示例中有直观对比:gql方式(document-builder_document.ts)可以显式指定操作名(如pokemonsAndTrainers)、在一个文档中同时包含 query 与 mutation,甚至用数组语法表达“同一字段的多别名执行”(如addPokemon以addAngryPikachu/addAngryCharizard两个别名各执行一次);而静态文档的Graffle.gql完整能力见 document-builder_static.ts(包含命名变量$('pokemonName').required()、默认值$.default('Ash')、必选/可选控制、混合 query+mutation 文档、SDDM 类型安全等)。
一句话总结:根级构建器是“快、简、单操作”的默认选择,gql是“全、细、多操作”的完整工具。
七、源码级原理:根级方法是如何生成的
为了不把根级构建器当黑盒使用,这里给出其生成链路:
- 配置入口:生成器配置中的
methodsOrganization控制方法的组织方式(configInit.ts):logical(默认true):按操作类型(query/mutation)组织,产出query、mutation命名空间;domains(默认false):按资源/领域组织,如client.pokemon.findByName(...)(见 domains 示例)。 示例仓库的 graffle.config.ts 同时启用了两者,其中 domains 用规则把pokemonByName→pokemon.findByName、addPokemon→pokemon.create等字段映射到领域命名空间。
- 接口生成:
ModuleGeneratorMethodsRoot(MethodsRoot.ts)产出BuilderMethodsRoot(含query/mutation属性),renderRootType为每个根类型生成QueryMethods/MutationMethods接口,其中固定包含$batch与__typename两个特殊方法,然后renderFieldMethods为每个根字段生成一个方法(选择集受NoExcess约束、返回MethodReturn类型)。 - 静态构建器:生成模块 document.ts 通过
createStaticRootType(QUERY/MUTATION, { sddm })创建query/mutation运行时对象,接口注释明确说明其“编译期生成文档、零运行时开销”的定位(见 document.ts)。 - 文档校验:JSDoc 模板(jsdoc.ts)为生成接口统一添加
$batch、__typename、根属性等说明,并在 IDE 悬停时展示 schema 信息(类型、kind、可空性、参数个数等,见 methods-root.ts)。
因此,你在编辑器中看到的每个字段方法的类型签名(如pokemonByName: Preflight<..., (selectionSet: NoExcess<$SelectionSet, SelectionSets.Query.pokemonByName<...>>) => MethodReturn<...>>,见 methods-root.ts)都是生成器根据 schema 实时计算出来的——schema 变了,方法签名、变量类型、结果类型都会同步变化。
八、验证与继续探索
本文所有输出均可在仓库中直接验证:
- 源码示例:document-builder_root-level-builders.ts;
- 官方输出(运行
show(doc)的实际结果):document-builder_root-level-builders.output.txt,其中六段输出与本文六个doc一一对应; - 网页版示例:root-level-builders.md。
建议继续阅读的关联示例:
- batch 示例:客户端实例下的
query.$batch批量请求; - root field 示例:
client.query.pokemons({...})单根字段执行; - arguments 示例:复杂参数(
filter: { name: { in: [...] } })写法; - static 示例:静态构建器的完整能力(命名变量、默认值、混合文档、SDDM);
- domains 示例:领域化方法组织。
总结
根级构建器是 Graffle 文档构建体系中“轻量快捷”的一档:const { mutation, query } = Graffle即可开始,字段选择用true、参数用$、批量用$batch、保留字参数用$type/$class转义,变量声明与类型全部由 Graffle 自动推导。它把“写 GraphQL 文档”变成了“写类型安全的 TypeScript 对象”,同时保留了完整的编译期校验与 schema 同步能力。当你的需求升级到命名操作、多操作同文档或精细控制文档结构时,再切换到Graffle.gql()即可——两者互补,共同构成 Graffle 的完整文档构建体验。
- 后端
【免费下载链接】graffle
Simple GraphQL Client for JavaScript. Minimal. Extensible. Type Safe. Runs everywhere.
相关推荐
Graffle 根级构建器(Root-Level Builders)指南:用 query / mutation / $batch 以 TypeScript 语法构建 GraphQL 文档
Graffle 根级构建器(Root Level Builders)指南:用 query / mutation / $batch 以 TypeScript 语法
后端Graffle 根级构建器(Root-Level Builders)实战:用 query / mutation 与 $batch 类型安全地构建 GraphQL 文档
Graffle 根级构建器(Root Level Builders)实战:用 query / mutation 与 $batch 类型安全地构建 GraphQL
后端Graffle 根级构建器实战:用 query / mutation 根方法与 $batch 构建类型安全的 GraphQL 文档
Graffle 根级构建器实战:用 query / mutation 根方法与 $batch 构建类型安全的 GraphQL 文档 本篇以 Graffle 的
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考