- 后端
【免费下载链接】graffle
Simple GraphQL Client for JavaScript. Minimal. Extensible. Type Safe. Runs everywhere.
本篇指南聚焦 Graffle 的实例 API(Instance API)——即创建客户端实例后,通过client.gql()方法发送 GraphQL 文档的完整用法。它是 Graffle 文档体系中的"发送"环节:在静态构建器(Graffle.gql())构建出文档之后,实例 API 负责把文档交给已配置 transport 的客户端执行,并提供按操作名调用的类型化方法与.$send()灵活入口。读完本文,你将掌握client.gql()支持的全部输入形态(字符串、对象、预构建文档、遗留类型)、Document Sender 发送器的两种执行方式、变量类型安全的工作机制,以及背后的源码级实现原理。
前置阅读:本文建立在静态文档构建之上。建议先阅读该指南理解文档如何构建,本文专注讲解如何使用客户端实例发送文档。
实例 API 概述:从文档到发送器
一旦你拥有了一个 Graffle 客户端实例,client.gql()就是发送 GraphQL 文档的入口。它属于实例 API——与静态构建器Graffle.gql()不同,它要求客户端已经完成配置(至少注册了 transport)。
import { Graffle } from 'graffle' const client = Graffle.create().transport({ url: 'https://api.example.com/graphql', }) // 发送一个 GraphQL 字符串 const sender = client.gql(` query getPokemon($name: String!) { pokemonByName(name: $name) { name hp } } `) // 使用操作名执行 const pokemon = await sender.getPokemon({ name: 'Pikachu' })从源码看,gql是 ClientBase 上声明的方法(gql: GqlMethod<$Context>),其运行时实现在 client.ts 的createWithContext中:先归一化参数,再调用createDocumentSender生成发送器,发送器内部通过sendRequest(context, request)进入请求管线。整个调用链是client.gql(doc)→GqlMethod.normalizeArguments→createDocumentSender(executeOperation)→executeOperation→sendRequest。
client.gql() 接受什么:四种输入形态
client.gql()接受与静态构建器完全相同的输入(字符串、对象),同时额外支持预构建文档与遗留类型。以下是完整对照:
字符串(string)
GraphQL 语法字符串,配合 GraphQLSP 等工具可获得类型推断:
const sender = client.gql(`query { pokemons { name } }`) await sender.$send()对象(object)
TypeScript 对象语法,基于 schema 的类型安全选择集:
const sender = client.gql({ query: { getPokemons: { pokemons: { name: true, hp: true }, }, }, }) await sender.getPokemons()预构建文档(pre-built)
由静态构建器(10_static.md)构建的文档直接传给实例:
import { Graffle } from './graffle/_.js' const doc = Graffle.query.pokemons({ name: true, hp: true }) const client = Graffle.create() await client.gql(doc).$send()遗留类型(legacy)
来自graphql包的DocumentNode、TypedQueryDocumentNode,以及 Graffle 自己的TypedDocument.String:
import { parse } from 'graphql' const doc = parse(` query pokemonByName($name: String!) { pokemonByName(name: $name) { name hp } } `) await client.gql(doc).$send({ name: 'Pikachu' }) import { type TypedQueryDocumentNode } from 'graphql' type PokemonDocument = TypedQueryDocumentNode< { pokemonByName: { name: string; hp: number } }, { name: string } > const typedDoc = parse(` query pokemonByName($name: String!) { pokemonByName(name: $name) { name hp } } `) as PokemonDocument const result = await client.gql(typedDoc).$send({ name: 'Pikachu' }) import { type TypedDocument } from 'graffle' type PokemonQuery = TypedDocument.String< { pokemonByName: { name: string; hp: number } }, { name: string } > const data = await client.gql<PokemonQuery>(` query pokemonByName($name: String!) { pokemonByName(name: $name) { name hp } } `).$send({ name: 'Pikachu' })类型安全分级:
| 类型 | 来源 | 类型安全 | 说明 |
|---|---|---|---|
DocumentNode | graphql包 | 无 | 仅运行时,无类型信息 |
TypedQueryDocumentNode | graphql包 | 有 | 类型安全的文档节点 |
TypedDocument.String | Graffle | 有 | 类型安全的字符串文档 |
这些类型通常由 GraphQL Code Generator 之类的代码生成工具产出。若希望不经过代码生成就获得原生类型安全,请改用静态构建器。
源码视角:GqlMethod 的类型重载
gql.ts 中的GqlMethod接口通过多条重载精确覆盖上述四种输入:
- 字符串重载:
string分支在配置了 GlobalRegistry 时走ParseGraphQLString解析出TypedFullDocument,否则退化为UntypedSender; - 对象重载:通过
ParseGraphQLObject把内联文档对象解析为类型化文档; - TypedDocumentLike 重载:
TypedDocumentNode、TypedDocumentString等直接生成对应发送器。
归一化逻辑见 gql.ts 中的normalizeArguments:判断依据是参数是否为字符串、是否含有definitions(DocumentNode)或__meta__(TypedDocumentNode)属性,从而区分"类型化文档"与"文档对象"两条处理路径。
值得注意的两个工程细节:
- 模板字面量语法被明确拒绝。运行时实现(client.ts)会检测模板字面量调用并抛出
Template literal syntax is not supported. Use call expression syntax instead错误,测试用例见 gql.test.ts。正确写法是gql('query { id }')而非gql\query { id }``。 - SDDM 文档的编译期校验。gql.ts 中
ValidateSDDMRequirement类型会在文档标注RequiresSDDM=true(例如携带自定义标量元数据)时,检查客户端是否配置了schema.map——缺失时直接产生类型错误"this document requires SDDM but your client configuration lacks it",防止 SDDM 文档被无 SDDM 能力的客户端执行。对应类型级断言见 gql.test-d.ts。
Document Sender:发送器的两种执行方式
调用client.gql()后返回的是一个Document Sender对象,提供两种执行操作的方式:操作方法(每个命名操作一个类型化方法)与$send静态方法。
操作方法(Operation Methods)
文档中的每个命名操作都会变成发送器上的一个方法,方法名即操作名:
const sender = client.gql(` query getPokemon($name: String!) { pokemonByName(name: $name) { name hp } } query getPokemons { pokemons { name } } `) const pokemon = await sender.getPokemon({ name: 'Pikachu' }) const pokemons = await sender.getPokemons()对象语法同样支持多操作,注意用$('name').required()声明必填变量:
import { $ } from 'graffle' const sender = client.gql({ query: { getPokemon: { pokemonByName: { $: { name: $('name').required() }, name: true, hp: true, }, }, getPokemons: { pokemons: { name: true, }, }, }, }) const pokemon = await sender.getPokemon({ name: 'Pikachu' }) const pokemons = await sender.getPokemons()变量规则:
- 必填变量——必须提供:
.getPokemon({ name: 'Pikachu' }) - 可选变量——可以省略:
.getPokemons()或.getPokemons({ filter: { type: 'FIRE' } }) - 无变量——无参调用:
.getPokemons()
TypeScript 会根据文档自动强制这些约束。类型定义在 DocumentSender.ts 中清晰可查:RequiredVarsNamedExecutor要求(variables: $Variables)必传,OptionalVarsNamedExecutor声明(variables?: $Variables)可省略,NoVarsNamedExecutor则完全无参——三者由OperationToNamedExecutor按变量种类(none/optional/required)自动选择。
运行时的代理实现:发送器并非预生成所有方法,而是由createDocumentSender用JavaScript Proxy动态实现——见 DocumentSender.ts:对任意字符串属性(除$send外)返回(variables?) => executeOperation(prop, variables),于是sender.getPokemon(...)在运行时被转换为一次以getPokemon为操作名的执行。这意味着操作方法的"类型安全"完全由编译期类型提供,运行时不持有方法列表,任何名字都能被代理接受(但类型层面会拦截非法操作名)。
$send 静态方法(Static Send Method)
$send提供运行时灵活性,支持四种调用形态:无参、仅变量、仅操作名、操作名+变量。
字符串语法:
await client.gql('query getPokemons { pokemons { name } }').$send() await client.gql( 'query getPokemon($name: String!) { pokemonByName(name: $name) { hp } }', ).$send( { name: 'Pikachu' }, ) const sender = client.gql(` query getPokemon($name: String!) { pokemonByName(name: $name) { name hp } } query getPokemons { pokemons { name } } `) await sender.$send('getPokemon', { name: 'Pikachu' }) await sender.$send('getPokemons')对象语法:
import { $ } from 'graffle' await client.gql({ query: { getPokemons: { pokemons: { name: true }, }, }, }).$send() await client.gql({ query: { getPokemon: { pokemonByName: { $: { name: $('name').required() }, hp: true, }, }, }, }).$send({ name: 'Pikachu' }) const sender = client.gql({ query: { getPokemon: { pokemonByName: { $: { name: $('name').required() }, name: true, hp: true, }, }, getPokemons: { pokemons: { name: true }, }, }, }) await sender.$send('getPokemon', { name: 'Pikachu' }) await sender.$send('getPokemons')$send的重载分发逻辑见 DocumentSender.ts 运行时实现:当第一个参数是字符串时视为操作名,否则视为变量对象。对应的类型重载包括:
- 单操作无变量(
SingleOpNoVarsStaticExecutor):(operationName?) => Promise<...>; - 单操作可选变量(
SingleOpOptionalVarsStaticExecutor):()、(variables?)、(operationName, variables?)三种签名; - 单操作必填变量(
SingleOpRequiredVarsStaticExecutor):(variables)、(operationName, variables); - 多操作(
MultiOpStaticExecutor):泛型签名按操作名通过Extract判别,从而对每个操作名推导出精确的变量与返回类型; - 未类型化文档(
UntypedStaticExecutor):接受任意操作名与变量,返回Promise<unknown>。
匿名操作:只能 $send()
匿名操作(没有操作名)只支持.$send(),因为操作方法是按名字注册的:
const sender = client.gql(`query { pokemons { name } }`) await sender.$send()const sender = client.gql({ query: { pokemons: { name: true }, }, }) await sender.$send()发送前的 Preflight 检查
值得留意的是,发送器的$send类型上还包裹了Configuration.Check.Preflight——见 DocumentSender.ts 的SenderStatic。当客户端未注册任何 transport、或当前 transport 未就绪时,$send在类型层面就会变成对应的错误类型(PreflightCheckNoTransportsRegistered、PreflightCheckTransportNotReady<...>),而不是等到运行时才报错;对应的类型级断言可在 gql.test-d.ts 末尾的 transport 测试段找到。
操作名与变量的配合:从文档到请求
变量与操作名的绑定关系是发送的核心。以下展示一个同时包含必填变量与带默认值变量的示例:
字符串语法:
const sender = client.gql(` query pokemonDetails($name: String!, $includeStats: Boolean = false) { pokemonByName(name: $name) { name hp @include(if: $includeStats) attack @include(if: $includeStats) } } `) await sender.pokemonDetails({ name: 'Pikachu', includeStats: true, })对象语法(用$修饰符表达同样的约束):
import { $ } from 'graffle' const sender = client.gql({ query: { pokemonDetails: { pokemonByName: { $: { name: $('name').required(), includeStats: $('includeStats').optional().default(false), }, name: true, hp: { $include: $('includeStats'), }, attack: { $include: $('includeStats'), }, }, }, }, }) await sender.pokemonDetails({ name: 'Pikachu', includeStats: true, })源码视角:变量输入种类如何决定调用签名
发送器的调用签名并非手工编写,而是由 graphql-kit 的 typed 模块 中的GetVariablesInputKind类型按变量对象结构自动推导,输出三值:'none'(无变量)、'optional'(可选变量)、'required'(必填变量)。推导规则依次是:变量为never→ none;带索引签名 → optional;含必填键 → required;仅有可选键 → optional;无键 → none。这套分类正是 DocumentSender.ts 中所有 executor 类型分支判定的依据。
类型级测试 gql.test-d.ts 系统性地验证了这些约束,包括:
- 无变量文档:
$send()合法,$send({})报@ts-expect-error; - 必填变量文档:
$send('getById', { id: '' })合法,缺变量($send('getById'))、类型错误($send({ id: 0 }))都会被类型系统拒绝; - 多操作文档:必须提供操作名(
$send()报错),操作名错误($send('bad'))被拒绝; - 未类型化文档(
as string):$send()、$send('anyName')、$send({ any: 'vars' })均返回unknown。
真实示例与发送策略建议
仓库的 examples 目录提供了可直接对照的完整示例:
- gql_gql-string.ts:使用字符串文档发送请求(
graffle.gql(...).$send()),展示了匿名查询 +$send()的最简用法; - gql_gql-document-node.ts:使用
parse()产出的 DocumentNode 发送请求,并叠加了Throws与OpenTelemetry扩展,演示了遗留类型在真实项目中的组合用法。
实践中的选择建议:
- 用操作方法还是
$send?操作方法是静态调用的首选——IDE 自动补全、参数类型检查、返回类型推导都最完整;$send适合操作名在运行时才确定、或需要在同一次发送中动态切换操作的场景,如sender.$send('getPokemon', { name })。 - 用字符串还是对象?字符串适合从 GraphQL Playground 粘贴、迁移既有文档;对象适合需要 IDE 自动补全全部字段、程序化生成文档的场景。两者在类型安全上等价,按工作流偏好选择即可。
- 类型安全最大化:优先使用静态构建器产出的预构建文档或
TypedDocument.String;DocumentNode仅用于纯运行时场景,因为它不携带结果与变量类型。
最后提醒:本文介绍的client.gql()依赖客户端实例的 transport 配置。若你使用的是生成的类型化客户端,其顶层gql方法行为一致,但类型会叠加 schema 提供的 GlobalRegistry 约束;未配置 transport 时发送会在类型层即被 Preflight 检查拦截(见上文),运行时也会抛出No transport selected。
- 后端
【免费下载链接】graffle
Simple GraphQL Client for JavaScript. Minimal. Extensible. Type Safe. Runs everywhere.
相关推荐
Graffle 使用 GraphQL 字符串文档发送请求:graffle.gql() 实战指南
Graffle 使用 GraphQL 字符串文档发送请求:graffle.gql 实战指南 GraphQL 字符串是最直观、最接近原生 GraphQL 语法的文
后端Graffle 实战:使用 `gql()` 字符串文档发送 GraphQL 请求
Graffle 实战:使用 gql 字符串文档发送 GraphQL 请求 导读 Graffle 是一个极简、可扩展、类型安全且跨平台运行的 JavaScript
后端Graffle 使用 GraphQL 字符串发送请求:gql() 基础用法与 TypedDocument.String 类型安全实践
Graffle 使用 GraphQL 字符串发送请求:gql 基础用法与 TypedDocument.String 类型安全实践 导读 Graffle 的 gq
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考