- 后端
【免费下载链接】graffle
Simple GraphQL Client for JavaScript. Minimal. Extensible. Type Safe. Runs everywhere.
导读
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,各字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
pattern | string \| RegExp | 匹配根字段名。字符串为精确匹配,正则可用捕获组供path/methodName引用 |
path | string \| string[] \| null | 命名空间路径;省略时默认'.'(根级);数组产生多个命名空间别名;null表示丢弃该字段 |
methodName | string \| string[] \| Function | 命名空间内的方法名;省略时默认使用字段原名;数组产生方法别名;函数可依据fieldName、operationType、正则match动态决定 |
consume | boolean | 默认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):
- 未使用规则(unused rules):某条规则的
pattern在 schema 中未命中任何字段,说明可能存在拼写错误或冗余规则; - 规则遮蔽(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.
相关推荐
Graffle 领域(Domains)方法组织:按资源分组 GraphQL 客户端方法的生成与配置指南
Graffle 领域(Domains)方法组织:按资源分组 GraphQL 客户端方法的生成与配置指南 导读 Graffle 是一款极简、可扩展、类型安全的 J
后端Sphinx 中 admonition 内嵌 figure 的 LaTeX 渲染机制:no_latex_floats 与 `[H]` 强制定位原理
Sphinx 中 admonition 内嵌 figure 的 LaTeX 渲染机制:no_latex_floats 与 H 强制定位原理 在 Sphinx 生
后端Graffle 域分组方法组织(Domains):按资源域组织 GraphQL Client 方法与生成器配置实战
Graffle 域分组方法组织(Domains):按资源域组织 GraphQL Client 方法与生成器配置实战 本文围绕 Graffle 的域分组(Doma
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考