☰
Graffle 按领域(Domain)组织 GraphQL 客户端方法:从配置规则到生成代码的完整指南
2026/10/10 5:24:34 网站建设 项目流程
  • 后端

【免费下载链接】graffle

Simple GraphQL Client for JavaScript. Minimal. Extensible. Type Safe. Runs everywhere.

项目地址:https://gitcode.com/gh_mirrors/gr/graffle
点击查看免费下载

导读

Graffle 是一款极简、可扩展、类型安全的 GraphQL 客户端。默认情况下,它按操作类型(query / mutation)组织根字段方法;而本文要讲解的领域(Domain)组织方式则把方法按资源/业务域分组,例如把pokemonByName、pokemons、addPokemon归入client.pokemon命名空间,把trainerByName、trainers归入client.trainer。本文以仓库中的 55_document-builder/domains 示例 为主线,结合生成器源码(Domains.ts、MethodsRoot.ts)与配置类型定义(configInit.ts),完整覆盖领域分组的配置规则、正则捕获组、别名、冲突处理、生成产物结构等内容,读完后你可以独立为自己的 Graffle 项目配置并落地领域化方法组织。

什么是领域化方法组织

在 Graffle 生成客户端时,根操作方法有两种组织方式(见 configInit.ts 中methodsOrganization的说明):

  • 逻辑组织(logical):按query/mutation分组,默认开启(@defaultValue true);
  • 领域组织(domains):按资源/业务域分组,默认关闭(@defaultValue false),需要显式配置规则。

两种方式可以同时开启。领域组织的核心思路是:不关心一个字段是查询还是变更,只关心它属于哪个业务实体。以 Pokemon 示例 schema 为例,未开启领域组织时,你只能写出:

client.query.pokemonByName({ $: { name: 'Pikachu' }, name: true }) client.query.pokemons({ name: true }) client.mutation.addPokemon({ $: { name: 'Charmander', type: 'fire' } })

开启领域组织后,同一批字段被重组成资源导向的调用形态:

client.pokemon.findByName({ $: { name: 'Pikachu' }, name: true }) client.pokemon.list({ name: true }) client.pokemon.create({ $: { name: 'Charmander', type: 'fire' } })

这正是 domains 示例 所演示的内容:query.pokemonByName变成pokemon.findByName,query.pokemons变成pokemon.list,而query.trainerByName变成trainer.findByName。

示例运行效果

示例(document-builder_domains.ts)先创建客户端,再按领域调用四个方法,并把结果通过showJson输出:

import { Graffle } from '../$/graffle/_.js' const client = Graffle.create() const pikachu = await client.pokemon.findByName({ $: { name: `Pikachu` }, name: true, hp: true, attack: true }) const allPokemon = await client.pokemon.list({ name: true, type: true }) const ash = await client.trainer.findByName({ $: { name: `Ash` }, name: true, pokemon: { name: true } }) const allTrainers = await client.trainer.list({ name: true }) showJson({ pikachu, allPokemon, ash, allTrainers })

对应的实际运行输出(见 domains.md)为:

{ "pikachu": [ { "name": "Pikachu", "hp": 35, "attack": 55 } ], "allPokemon": [ { "name": "Pikachu", "type": "electric" }, { "name": "Charizard", "type": "fire" }, { "name": "Squirtle", "type": "water" }, { "name": "Bulbasaur", "type": "grass" }, { "name": "Caterpie", "type": "bug" }, { "name": "Weedle", "type": "bug" } ], "ash": { "name": "Ash", "pokemon": [ { "name": "Pikachu" }, { "name": "Charizard" } ] }, "allTrainers": [ { "name": "Ash" }, { "name": "Misty" }, { "name": "Brock" }, { "name": "Gary" } ] }

可以看到,findByName的返回类型是数组(因为底层pokemonByName字段返回[Pokemon!]),而领域分组只改变方法挂载位置与命名,不改变字段本身的类型语义。

开启领域组织:配置规则

领域分组不是自动推断的,必须通过Generator.configure显式声明规则。仓库中的 graffle.config.ts 给出了完整示例:

import { Generator } from 'graffle/generator' const config = Generator.configure({ schema: { type: 'sdl', sdl: '../tests/_/fixtures/schemas/pokemon/schema.graphql', }, outputDirPath: './$/graffle', defaultSchemaUrl: new URL('http://localhost:3000/graphql'), methodsOrganization: { logical: true, // Keep query/mutation organization domains: { rules: [ // Pokemon domain { pattern: 'pokemonByName', path: 'pokemon', methodName: 'findByName' }, { pattern: 'pokemons', path: 'pokemon', methodName: 'list' }, { pattern: 'addPokemon', path: 'pokemon', methodName: 'create' }, // Trainer domain { pattern: 'trainerByName', path: 'trainer', methodName: 'findByName' }, { pattern: 'trainers', path: 'trainer', methodName: 'list' }, // Battle domain { pattern: 'battles', path: 'battle', methodName: 'list' }, // Being domain (polymorphic) { pattern: 'beings', path: 'being', methodName: 'list' }, ], }, }, lint: { missingGraphqlSP: false, }, }) export default config

规则逐条对照 schema 的根字段名(pattern)匹配,把命中的字段搬运到指定的命名空间(path),并(可选)重命名方法(methodName)。上面的配置即产生了示例中的pokemon.findByName、pokemon.list、pokemon.create、trainer.findByName、trainer.list、battle.list、being.list。

规则字段详解

FieldGroupingRule的完整定义见 configInit.ts,各字段如下:

字段类型说明
patternstring \| RegExp匹配根字段名。字符串为精确匹配,正则可用捕获组供path/methodName引用
pathstring \| string[] \| null命名空间路径;省略时默认'.'(根级);数组产生多个命名空间别名;null表示丢弃该字段
methodNamestring \| string[] \| Function命名空间内的方法名;省略时默认使用字段原名;数组产生方法别名;函数可依据fieldName、operationType、正则match动态决定
consumeboolean默认false(字段可被多条规则命中);true表示命中后停止继续匹配后续规则;true+path: null表示丢弃字段且不产生告警

path 的取值形态

  • 单层:path: 'pokemon'→graffle.$.pokemon.*;
  • 点分嵌套:path: 'api.v2.pokemon'→graffle.$.api.v2.pokemon.*(生成器按.拆分为多级命名空间);
  • 根级:path: '.'→graffle.$.pokemonByName(方法直接挂在根上,不进入任何领域);
  • 别名数组:path: ['pokemon', 'poke']→ 同时生成graffle.$.pokemon.*与graffle.$.poke.*;
  • 丢弃:path: null→ 该字段不生成方法,且不进入"未匹配字段"告警。

正则捕获组的引用

当pattern为正则时,可在path/methodName中通过$name、$1引用捕获组,或使用${transform:ref}做大小写转换。例如:

{ pattern: /^(?<resource>\w+)ByName$/, path: '$resource' } // pokemonByName → graffle.$.pokemon.* // trainerByName → graffle.$.trainer.* { pattern: /^pokemonBy(\w+)$/, methodName: 'getBy$1' } // pokemonByName → getByName { pattern: /^(?<resource>\w+)s$/, path: '$resource', methodName: '${capFirst:resource}List' } // pokemons → pokemon.List(capFirst 转换)

可用的转换(见 MethodsRoot.ts 的注释与 replaceCaptures 实现)包括:kebab、pascal、camel、snake、constant、title、upper、lower、capFirst、uncapFirst。

methodName 的函数形态

methodName可以是函数,接收(fieldName, operationType, match?)三个参数,例如按操作类型自动命名:

methodName: (fieldName, operationType) => operationType === 'query' ? 'get' : 'create'

配合正则捕获时还能拿到match.groups做更精细的映射:

methodName: (fieldName, operationType, match) => match?.groups?.action === 'add' ? 'create' : 'update'

冲突处理:onMergeConflict

当多个子命名空间合并到同一属性、或同一命名空间内多个字段映射到同名方法时,会产生冲突。例如pokemon.query与pokemon.list合并为pokemon后,若两边都有同名方法就会冲突。DomainGroupingConfig.onMergeConflict提供三种策略(configInit.ts):

值行为实现状态
'fail'抛出错误,提示在命名空间内使用唯一方法名已实现(默认值)
'merge'自动递增重名(如get→get、get2)尚未实现(源码中会抛"not yet implemented")
'drop'保留首个出现,丢弃重复项尚未实现

冲突检测逻辑位于两处:生成阶段的 MethodsRoot.ts 的 groupFieldsByDomain(按namespaceKey + methodName检测并throw),以及命名空间树构建阶段的 MethodsRoot.ts(同一命名空间内methodName → fieldName冲突检测)。这意味着务必保证每个命名空间内的方法名唯一,否则生成会直接失败。

规则匹配语义与开发者告警

多规则命中与 consume

默认情况下一个字段可以命中多条规则(进入多个命名空间),实现"多分类"。例如:

{ pattern: /^date/, path: 'feat.date' }, { pattern: /^date/, path: 'type.scalar' }, // dateField 同时出现在 feat.date 与 type.scalar

若希望"先匹配先得",用consume: true:

{ pattern: 'specificField', path: 'special', consume: true }, { pattern: /Field/, path: 'generic' }, // specificField 只在 special,其余 *Field 在 generic

需要"完全隐藏某个字段"时,用consume: true+path: null(丢弃且不告警)。

未匹配规则与规则遮蔽告警

生成器会在构建时输出两类开发告警(MethodsRoot.ts 与 L698-L719):

  1. 未使用规则(unused rules):某条规则的pattern在 schema 中未命中任何字段,说明可能存在拼写错误或冗余规则;
  2. 规则遮蔽(precedence warning):前面的正则/字符串规则会抢先匹配后面规则的 pattern(例如索引 0 的正则命中了索引 1 的字符串 pattern),提示你调整规则顺序,把更具体的规则放在前面。

生成产物结构

启用领域组织后,生成器会输出独立的domains/模块目录。仓库中已生成的真实产物位于 examples/$/graffle/modules/domains,结构如下:

domains/ ├── __.ts # 根导出:export * as pokemon from './pokemon/__.js' 等 ├── battle/ │ ├── __.ts │ └── methods.ts ├── being/ │ ├── __.ts │ └── methods.ts ├── pokemon/ │ ├── __.ts │ └── methods.ts └── trainer/ ├── __.ts └── methods.ts

每个命名空间由 Domains.ts 生成两个文件:

  • methods.ts:真实的方法实现。每个方法是从graffle/extensions/document-builder导入的预柯里化 helper 的调用,例如 pokemon/methods.ts:
import { $$mutation, $$query } from 'graffle/extensions/document-builder' export const findByName = $$query('pokemonByName') export const list = $$query('pokemons') export const create = $$mutation('addPokemon')

也就是说,findByName本质上是$$query('pokemonByName')的别名,$$query/$$mutation把字段名"预绑定"进 Document Builder 的执行管线,再被领域命名空间重新导出(Domains.ts)。方法前还会附带从 schema 字段描述生成的 JSDoc(包含 GraphQL 签名、类型、父类型、路径、可空性、列表、参数数量等)。

  • __.ts:命名空间索引文件,export * from './methods.js',并在存在别名时追加export { primary as alias } from './methods.js'(generateNamespaceIndexFile)。

  • 根domains/__.ts:把每个命名空间以export * as <camelCase路径> from './<路径>/__.js'的形式导出(generateRootIndexFile),例如点分路径api.v2.pokemon会被导出为apiV2Pokemon。

类型层面的支持

领域组织不仅改变运行时方法位置,也生成对应的类型接口。在 MethodsRoot.ts 中,启用methodsOrganization.domains后,BuilderMethodsRoot会把每个顶层命名空间作为属性挂出,例如pokemon: PokemonMethods<$Context>;而嵌套命名空间(如api.v2.pokemon)会递归生成中间接口(如ApiV2Methods→ApiV2PokemonMethods),最终把所有叶子命名空间的方法合并进对应的接口(generateNamespaceInterfaces)。

每个领域方法的类型签名与逻辑组织一致:使用$Context泛型贯穿配置检查(Preflight)、选择集类型校验(NoExcess+SelectionSets)、Schema 驱动数据映射推断(SchemaDrivenDataMap)以及输出处理(HandleOutputDocumentBuilderRootField),因此在client.pokemon.findByName(...)上依然能获得完整的类型推断、参数提示与多余字段报错。

小结

领域化方法组织是 Graffle 生成器的一项可配置能力:通过 graffle.config.ts 中的methodsOrganization.domains.rules,你可以把扁平化的 GraphQL 根字段重新编排为面向资源的 API 形态(client.pokemon.*、client.trainer.*),并利用正则捕获组、别名数组、consume与onMergeConflict精细控制分组行为。其生成逻辑集中在 Domains.ts 与 MethodsRoot.ts,生成的domains/模块直接复用graffle/extensions/document-builder的$$query/$$mutation预柯里化 helper,保证了类型安全与运行时一致性。你可以对照 55_document-builder/domains 示例 及其源码 快速验证效果,再根据自己的 schema 编写规则即可落地。

  • 后端

【免费下载链接】graffle

Simple GraphQL Client for JavaScript. Minimal. Extensible. Type Safe. Runs everywhere.

项目地址:https://gitcode.com/gh_mirrors/gr/graffle
点击查看免费下载
上一篇:expo-router 的 Color 工具:基于 Android 官方文档生成类型化平台颜色类型
下一篇:BabelDOC PDF翻译实战:4个场景配好参数,公式和双栏都不跑偏

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

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

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

立即咨询