ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TypeGraphQL Resolvers 完全指南:用 TypeScript 类与装饰器编写 Query、Mutation 与 Field Resolver

TypeGraphQL Resolvers 完全指南:用 TypeScript 类与装饰器编写 Query、Mutation 与 Field Resolver 后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载TypeGraphQL 在 types-and-fields 声明的对象类型之外允许开发者像编写普通类方法一样轻松创建 GraphQL 的 queries、mutations 与字段解析器field resolvers体验上非常接近 JavaSpring、.NETWeb API或 TypeScriptrouting-controllers这类经典 REST 框架的控制器。读完本文你将掌握 resolver 类的声明方式、参数与输入类型的使用技巧、字段解析器的两种写法以及如何在项目中利用依赖注入与类型系统写出可测试、类型安全的业务代码。Queries 与 MutationsResolver 类GraphQL 世界的控制器首先创建一个 resolver 类并用Resolver()装饰器标注它。这个类在角色上等同于经典 REST 框架中的 controllerResolver() class RecipeResolver {}Resolver()的实现位于 src/decorators/Resolver.ts它会把目标类的元数据收集进全局的MetadataStorage后续 schema 生成器正是基于这些元数据构建 GraphQL 的Query/Mutation类型。从源码可见Resolver()支持三种签名——无参、传入类型函数如of Recipe或直接传入类引用无参形式主要用于仅声明 query/mutation 的场景。Resolver 类本身是单例的整个应用共享同一个实例因此可以借助 DI 框架详见 dependency-injection注入 service 或 repository 等依赖也可以在类内部直接保存数据Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; }接下来把类方法定义为 query。例如添加一个返回全部菜谱的recipesqueryResolver() class RecipeResolver { private recipesCollection: Recipe[] []; async recipes() { // Fake async return await this.recipesCollection; } }这里还差两件事第一用Query装饰器把方法标记为 GraphQL query第二声明返回类型。由于方法是 async 的反射元数据系统看到的返回类型是Promise因此必须在装饰器参数里用returns [Recipe]明确告知它最终解析为一个Recipe对象类型数组。Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; Query(returns [Recipe]) async recipes() { return await this.recipesCollection; } }为什么必须写returns [Recipe]可以查看 src/helpers/resolver-metadata.ts 中getResolverMetadata的逻辑TypeGraphQL 会优先尝试从design:returntype反射元数据推断类型但 async 方法反射出来的是Promise而非具体类型所以泛型数组、Promise 包装等场景需要开发者显式提供返回类型函数这一机制同样作用于Query、Mutation与FieldResolversrc/decorators/FieldResolver.ts 内部通过findType做同样的推断兜底。参数声明Arg()行内写法query 通常带有参数——资源的 id、搜索关键词、分页设置等。TypeGraphQL 提供两种参数声明方式。第一种是行内写法使用Arg()装饰器。由于反射系统的限制必须在装饰器参数中重复参数名。同时可以传入defaultValue选项它会如实反映到生成的 GraphQL schema 中Resolver() class RecipeResolver { // ... Query(returns [Recipe]) async recipes( Arg(servings, { defaultValue: 2 }) servings: number, Arg(title, { nullable: true }) title?: string, ): PromiseRecipe[] { // ... } }从 src/decorators/Arg.ts 的源码看Arg的选项类型ArgOptions组合了DecoratorTypeOptions、DescriptionOptions、ValidateOptions与DeprecationOptions也就是说除了defaultValue和nullable还可以声明description在 schema 中生成参数描述、deprecationReason标记废弃参数以及配合 class-validator 使用的校验相关选项。参数声明ArgsType()参数类写法当参数超过 23 个时行内写法会让方法签名变得臃肿。此时可以定义参数类args class。它看起来与对象类型类几乎一致唯一区别是类上方的装饰器为ArgsType()ArgsType() class GetRecipesArgs { Field(type Int, { nullable: true }) skip?: number; Field(type Int, { nullable: true }) take?: number; Field({ nullable: true }) title?: string; }可选字段的默认值有两种声明途径在Field()装饰器里传defaultValue选项或者直接使用属性初始化器property initializer。无论哪种方式TypeGraphQL 都会把默认值写入 schema这样客户端发送 query 时就可以省略这些参数import { Min, Max } from class-validator; ArgsType() class GetRecipesArgs { Field(type Int, { defaultValue: 0 }) Min(0) skip: number; Field(type Int) Min(1) Max(50) take 25; Field({ nullable: true }) title?: string; // Helpers - index calculations get startIndex(): number { return this.skip; } get endIndex(): number { return this.skip this.take; } }⚠️ 注意defaultValue只对输入类别的参数和字段生效即Arg、ArgsType与InputType。给ObjectType或InterfaceType的字段设置defaultValue不会起作用因为它们是纯输出用途。参数类这种声明方式还天然支持校验配合 class-validator 的Min/Max等装饰器即可在参数解析阶段自动校验完整机制见 validation 文档。另外参数类或输入类中可以定义辅助字段与方法但严禁定义构造函数constructors——TypeGraphQL 会在内部自行创建 args / input 类的实例自定义构造函数会导致实例化失败。然后在 resolver 方法中使用参数类作为参数类型还可以用解构语法直接拿到单个参数变量而不必持有整个 args 对象Resolver() class RecipeResolver { // ... Query(returns [Recipe]) async recipes(Args() { title, startIndex, endIndex }: GetRecipesArgs) { // Example implementation let recipes this.recipesCollection; if (title) { recipes recipes.filter(recipe recipe.title title); } return recipes.slice(startIndex, endIndex); } }这段声明最终会在 SDL schema 中生成如下片段type Query { recipes(skip: Int 0, take: Int 25, title: String): [Recipe!] }可以看到skip 0、take 25正是由defaultValue与属性初始化器驱动生成的默认值。输入类型用InputType()描述 mutation 入参GraphQL mutation 的创建方式与 query 类似声明类方法、加Mutation装饰器、创建参数、按需提供返回类型。不过 mutation 通常使用input类型TypeGraphQL 允许像创建对象类型一样见 types-and-fields用InputType()装饰器来创建InputType() class AddRecipeInput {}为了借助 TypeScript 类型系统防止意外改变属性类型可以让输入类实现PartialRecipeInputType() class AddRecipeInput implements PartialRecipe {}随后用Field()声明需要的输入字段InputType({ description: New recipe data }) class AddRecipeInput implements PartialRecipe { Field() title: string; Field({ nullable: true }) description?: string; }从 src/decorators/InputType.ts 可以看出InputType支持可选的name自定义 schema 类型名默认取类名和description选项。之后就可以在 mutation 中使用AddRecipeInput了既可以像上面 query 那样行内用Arg()也可以作为 args 类的字段。此外我们常常需要访问 GraphQL 上下文此时使用Ctx()装饰器并可以传入自定义的Context接口Resolver() class RecipeResolver { // ... Mutation() addRecipe(Arg(data) newRecipeData: AddRecipeInput, Ctx() ctx: Context): Recipe { // Example implementation const recipe RecipesUtils.create(newRecipeData, ctx.user); this.recipesCollection.push(recipe); return recipe; } }因为该方法同步且显式返回RecipeTypeScript 反射可以正确推断类型所以这里可以省略Mutation()的类型标注。在 src/decorators/Ctx.ts 中可以看到Ctx还支持可选的propertyName参数用于只从 context 中解出指定属性注入参数。这段声明生成的 SDL 如下input AddRecipeInput { title: String! description: String }type Mutation { addRecipe(data: AddRecipeInput!): Recipe! }参数装饰器的另一大收益是可以去掉root这类多余参数否则需要按惯例用_前缀忽略从而让方法签名保持干净同时通过装饰器实现 GraphQL 层与业务代码的清晰分离resolver 及其方法就像普通 service 一样易于单元测试。字段解析器Field Resolversquery 与 mutation 并不是唯一的 resolver 类型。当对象类型中的字段需要额外计算或从数据库按需取数时例如user类型的posts字段就需要编写字段解析器。TypeGraphQL 中字段解析器与 query/mutation 非常相似——同样是 resolver 类上的方法但有几点差异。首先要在Resolver装饰器中通过类型函数声明当前解析的是哪个对象类型的字段Resolver(of Recipe) class RecipeResolver { // Queries and mutations }Resolver的typeFunc重载正是为此设计schema 生成器会依据它把该类的字段解析器方法挂到对应对象类型上。然后创建将成为字段解析器的类方法。比如Recipe对象类型中有一个averageRating字段需要根据ratings数组计算平均值Resolver(of Recipe) class RecipeResolver { // Queries and mutations averageRating(recipe: Recipe) { // ... } }接着用FieldResolver()装饰器标记该方法。由于字段类型已经在Recipe类定义中声明过了这里无需重复声明同时用Root装饰器注入 recipe 对象作为方法参数Resolver(of Recipe) class RecipeResolver { // Queries and mutations FieldResolver() averageRating(Root() recipe: Recipe) { // ... } }从 src/decorators/FieldResolver.ts 的源码可以看到FieldResolver支持三种签名无参、仅传AdvancedOptions、或传返回类型函数加选项它内部会尝试通过design:returntype反射推断返回类型推断失败也不抛错留给 schema 生成阶段兜底同时接受name、description、deprecationReason、complexity等选项。为了更强的类型安全可以让 resolver 类实现ResolverInterfaceRecipe接口。这是一个小助手类型定义于 src/typings/ResolverInterface.ts它会检查字段解析器方法如averageRating(...)的返回类型是否与Recipe类的averageRating属性类型一致并校验方法的第一个参数确实是Recipe类即 root 对象类型Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { // Queries and mutations FieldResolver() averageRating(Root() recipe: Recipe) { // ... } }averageRating字段解析器的完整实现示例Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { // Queries and mutations FieldResolver() averageRating(Root() recipe: Recipe) { const ratingsSum recipe.ratings.reduce((a, b) a b, 0); return recipe.ratings.length ? ratingsSum / recipe.ratings.length : null; } }内联字段解析器简单场景的快捷方式对于averageRating这类简单解析器或者行为类似别名alias的已废弃字段可以直接在对象类型类定义中内联创建字段解析器ObjectType() class Recipe { Field() title: string; Field({ deprecationReason: Use title instead }) get name(): string { return this.title; } Field(type [Rate]) ratings: Rate[]; Field(type Float, { nullable: true }) averageRating(Arg(since) sinceDate: Date): number | null { const ratings this.ratings.filter(rate rate.date sinceDate); if (!ratings.length) return null; const ratingsSum ratings.reduce((a, b) a b, 0); return ratingsSum / ratings.length; } }注意内联字段解析器还可以像 resolver 方法一样使用参数装饰器——上例中的averageRating就通过Arg(since)声明了一个输入参数。生成 schema 时它会表现为该字段的参数字段。不过当逻辑更复杂且带有副作用例如调用 API、从数据库取数时应该改用 resolver 类方法。这样就能利用依赖注入机制对测试非常友好import { Repository } from typeorm; Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { constructor( // Dependency injection private readonly userRepository: RepositoryUser, ) {} FieldResolver() async author(Root() recipe: Recipe) { const author await this.userRepository.findById(recipe.userId); if (!author) throw new SomethingWentWrongError(); return author; } }这里author字段并不存在于Recipe类的属性签名中但依然可以解析——TypeGraphQL 的规则是如果字段解析器的字段名在对应对象类型中不存在schema 生成器会自动用该名称创建一个新字段。这一特性非常适合纯计算型字段例如从ratings数组计算出的averageRating既避免了在类签名中污染额外属性又能为用户提供完整的 schema 字段。Resolver 继承Resolver 类的继承属于进阶主题TypeGraphQL 支持通过类继承复用 query、mutation 与字段解析器详细机制参见 inheritance 文档中的 Resolvers Inheritance 小节。仓库内 examples/resolvers-inheritance 提供了可运行的真实示例展示了父类定义通用解析器、子类按需覆盖的写法。真实示例上文所有代码都是为了教学目的编写的示意片段。仓库中提供了大量更完整、更贴近生产环境的可运行示例例如examples/simple-usage包含recipe.resolver.ts、recipe.type.ts、recipe.input.ts的完整入门项目并附带schema.graphql可以对照查看生成结果examples/authorization展示 resolver 与Authorized装饰器结合做权限控制examples/middlewares-custom-decorators展示如何用中间件和自定义装饰器增强 resolver 方法examples/typeorm-basic-usage演示字段解析器与 ORM 数据源配合的真实取数场景。每个示例目录下都配有schema.graphql可以直观对照代码声明与最终生成的 GraphQL schema是理解 resolver 机制最直接的辅助材料。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐TypeGraphQL Resolvers 完全指南用 TypeScript 类与装饰器构建 Query、Mutation 与 Field ResolverTypeGraphQL Resolvers 完全指南用 TypeScript 类与装饰器构建 Query、Mutation 与 Field Resolver后端GraphQLAPI设计TypeGraphQL Resolvers 实战指南用 TypeScript 类与装饰器编写 Query、Mutation 与 Field ResolversTypeGraphQL Resolvers 实战指南用 TypeScript 类与装饰器编写 Query、Mutation 与 Field Resolvers后端GraphQLAPI设计TypeGraphQL Resolvers 实战指南用 TypeScript 类与方法构建 Query、Mutation 与 Field ResolverTypeGraphQL Resolvers 实战指南用 TypeScript 类与方法构建 Query、Mutation 与 Field Resolver T后端GraphQLAPI设计上一篇sniffer vs bandwhich vs nethogs三大网络嗅探工具性能深度对比下一篇Jellium Desktop快捷键自定义教程打造专属操作方式创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表