尧图网站设计 尧图网站设计YAOTU DESIGN
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 允许开发者像编写普通 TypeScript 类方法一样创建 GraphQL 的 queries、mutations 与 field resolvers其风格类似 JavaSpring、.NETWeb API或 TypeScript 生态中的routing-controllers等 REST 控制器框架。本文将基于 TypeGraphQL 官方文档《Resolvers》的核心脉络结合仓库源码与真实示例完整讲解 Resolver 类的声明方式、参数定义Arg/ArgsType、输入类型InputType、上下文注入Ctx以及字段级解析器FieldResolver的底层机制帮助你直接用类与方法组织出一套可测试、可维护的 GraphQL 服务端代码。认识 Resolver 类GraphQL 世界的“控制器”在 types-and-fields.md 中我们学会了声明 GraphQL 的 object types而 resolver 则负责为这些类型提供数据。TypeGraphQL 中resolver 就是一个普通的类用Resolver()装饰器标记即可Resolver() class RecipeResolver {}这个类会像经典 REST 框架中的 controller 一样工作类上可以存放数据、注入依赖。你可以使用 DI 框架详见 dependency-injection.md注入 service 或 repository也可以在 resolver 类内部保存数据——文档明确保证 resolver 类在整个应用中是单例实例Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; }从源码看src/decorators/Resolver.ts 的Resolver()装饰器支持三种重载无参形式仅声明 resolver 类、传入ClassType对象类型或传入ClassTypeResolver类型函数如_of Recipe。无参形式下若之后使用FieldResolver会抛出未提供对象类型的错误这一点会在下文 field resolvers 部分展开。Queries 与 Mutations把方法变成 GraphQL 操作在 resolver 类中创建普通方法即可处理 query 和 mutation。以“返回所有 recipes”的查询为例Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; async recipes() { // 本例中为模拟异步 return await this.recipesCollection; } }要让该方法成为 GraphQL query需要做两件事加上Query装饰器将方法标记为 GraphQL query提供返回类型。由于方法是 async反射元数据系统得到的返回类型是Promise因此必须在装饰器参数中用returns [Recipe]显式声明它解析为一个Recipe对象类型的数组Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; Query(returns [Recipe]) async recipes() { return await this.recipesCollection; } }Query与Mutation在实现上是几乎对称的查看 src/decorators/Query.ts 与 src/decorators/Mutation.ts两者都通过getResolverMetadata解析返回类型再分别调用collectQueryHandlerMetadata/collectMutationHandlerMetadata把元数据写入全局 metadata storage。最终 schema 生成器src/schema/schema-generator.ts会消费这些元数据产出可执行的 GraphQL Schema。参数定义两种方式按需选择query 通常需要参数——资源 id、搜索关键词或分页设置。TypeGraphQL 支持两种参数定义方式。方式一Arg()内联定义使用Arg()装饰器在方法签名中内联声明参数。由于反射系统的限制需要在装饰器参数中重复一遍参数名。同时可以传入defaultValue选项该默认值会反映到 GraphQL schema 中Resolver() class RecipeResolver { // ... Query(returns [Recipe]) async recipes( Arg(title, { nullable: true }) title?: string, Arg(servings, { defaultValue: 2 }) servings: number, ): PromiseRecipe[] { // ... } }Arg的参数在 src/decorators/Arg.ts 中被定义为ArgOptions它由DecoratorTypeOptions、DescriptionOptions、ValidateOptions与DeprecationOptions组合而成因此除nullable、defaultValue外还支持description、deprecationReason以及校验相关选项配合 class-validator 使用见下文校验章节。当参数只有 23 个时这种方式简洁直接但参数一多方法签名就会变得臃肿。方式二ArgsType()参数类参数较多时可以用一个类来描述参数集合。它长得像 object type 类但顶部装饰器是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选项或直接使用属性初始化器。两种方式下 TypeGraphQL 都会在 schema 中反映默认值同时将该字段置为 nullable。需要特别注意的是defaultValue只对输入侧生效——即Arg、ArgsType、InputType对ObjectType与InterfaceType的输出字段不生效因为它们仅用于输出目的。参数类还可以携带辅助字段与方法例如分页索引的计算。但文档明确警告参数类与输入类中严禁定义构造函数因为 TypeGraphQL 会在内部自行实例化这些类。同时还可以搭配 class-validator 进行参数校验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; // 辅助计算属性 get startIndex(): number { return this.skip; } get endIndex(): number { return this.skip this.take; } }注意这里使用了get计算属性getter这是旧版文档中startIndex skip; endIndex skip take;字段写法在类实例化时序下的更安全替代——普通属性赋值在类被实例化时才会求值而 getter 每次访问都实时计算。校验的更多细节见 validation.md。然后在 resolver 方法中把参数类作为方法参数的类型并可用解构语法把单个参数直接取出为变量Resolver() class RecipeResolver { // ... Query(returns [Recipe]) async recipes(Args() { title, startIndex, endIndex }: GetRecipesArgs) { // 示例实现 let recipes this.recipesCollection; if (title) { recipes recipes.filter(recipe recipe.title title); } return recipes.slice(startIndex, endIndex); } }这段声明最终会在 schema SDL 中生成type Query { recipes(skip: Int 0, take: Int 25, title: String): [Recipe!] }可以看到skip、take的默认值0 与 25以及可空性都被完整反映到了 GraphQL schema 中客户端查询时可以省略这些参数。输入类型InputType()与 MutationGraphQL 的 mutation 可以用同样的方式创建声明类方法、加Mutation装饰器、按需提供返回类型、创建参数等。但 mutation 通常使用input类型因此 TypeGraphQL 允许像创建 object types 一样详见 types-and-fields.md只是把装饰器换成InputType()InputType() class AddRecipeInput {}还可以利用 TypeScript 的类型检查系统通过实现Partial类型来防止无意中改变属性的类型InputType() class AddRecipeInput implements PartialRecipe {}接着用Field()装饰器声明所需的输入字段InputType({ description: New recipe data }) class AddRecipeInput implements PartialRecipe { Field() title: string; Field({ nullable: true }) description?: string; }之后就能在 mutation 中使用AddRecipeInput类型——可以像上面的 query 示例一样内联使用Arg()也可以作为参数类的一个字段。若需要访问上下文使用Ctx()装饰器并配合用户自定义的Context接口Resolver() class RecipeResolver { // ... Mutation() addRecipe(Arg(data) newRecipeData: AddRecipeInput, Ctx() ctx: Context): Recipe { // 示例实现 const recipe RecipesUtils.create(newRecipeData, ctx.user); this.recipesCollection.push(recipe); return recipe; } }因为该方法同步且显式返回Recipe可以省略Mutation()的类型标注。生成的 SDL 如下input AddRecipeInput { title: String! description: String }type Mutation { addRecipe(data: AddRecipeInput!): Recipe! }Ctx在 src/decorators/Ctx.ts 中实现它支持传入可选的propertyName参数用于只提取 context 对象的某个属性例如Ctx(user) user: User。借助这些参数装饰器我们可以摆脱像root这样不必要的参数旧式 GraphQL 解析器签名中需要把它忽略通常用_前缀命名实现 GraphQL 层与业务代码的干净分离——resolver 及其方法表现得就像纯服务一样可以轻松进行单元测试。仓库中的真实示例可参考 examples/simple-usage/recipe.resolver.ts其中recipesquery、addRecipemutation 与参数类RecipeInput的组合与上述模式一一对应。Field Resolvers对象类型字段的解析器query 和 mutation 不是唯一的解析器类型。当 object type 的某个字段比如user类型中的posts字段需要从数据库拉取关联数据时就需要 field resolvers。TypeGraphQL 中它们与 query/mutation 很相似——同样是 resolver 类上的方法但有几点修改。声明对象类型与Root注入首先需要在Resolver装饰器中传入要解析字段的对象类型Resolver(of Recipe) class RecipeResolver { // queries and mutations }然后创建成为 field resolver 的类方法。例如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) { // ... } }Root在 src/decorators/Root.ts 中实现它通过design:paramtypes反射元数据推断父对象类型同样支持可选的propertyName参数来注入父对象的某个属性而非整个对象。ResolverInterfaceT编译期类型安全为了增强类型安全可以实现ResolverInterfaceRecipe接口。这是一个小助手类型定义在 src/typings/ResolverInterface.tsexport type ResolverInterfaceT extends object { [P in keyof T]?: (root: T, ...args: any[]) T[P] | PromiseT[P]; };它会检查 field resolver 方法如averageRating(...)的返回类型是否与Recipe类的averageRating属性类型匹配同时检查方法第一个参数是否为真正的对象类型Recipe类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; } }内联 Field Resolver简单计算的快捷方式对于像averageRating这类简单计算或行为类似别名的弃用字段可以直接把 field resolver 内联写在 object type 类定义中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 的方法参数也可以使用Arg等参数装饰器如上面的Arg(since)。复杂逻辑回到 Resolver 类方法但当逻辑更复杂、有副作用如 API 调用、数据库查询时应使用 resolver 类方法。这样可以利用依赖注入机制对测试非常有利import { Repository } from typeorm; Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { constructor( // 依赖注入 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; } }FieldResolver装饰器的实现见 src/decorators/FieldResolver.ts它支持AdvancedOptions含name、description、deprecationReason、complexity等并通过findType尝试从design:returntype反射元数据推断返回类型最终以kind: external的形式收集 field resolver 元数据。还有一个实用特性如果 field resolver 的名字在对象类型中不存在TypeGraphQL 会在 schema 中自动创建同名字段。这个特性对纯计算字段如由ratings数组算出的averageRating非常有用可以避免污染类的签名。真实示例可见 examples/simple-usage/recipe.resolver.ts 中的ratingsCount字段——它在Recipe类中并不存在而是由 resolver 动态创建的还展示了 field resolver 同样可以接收Arg参数如minRate的默认值 0。Resolver 继承Resolver 类的继承属于进阶主题涵盖在 inheritance.md#resolvers-inheritance 中。仓库的 examples/resolvers-inheritance 目录提供了可运行的继承示例person与recipe两个子 resolver 共享基类逻辑有兴趣可以对照阅读。总结TypeGraphQL 把 GraphQL resolver 变成了一等公民的 TypeScript 类与方法Query / MutationQuery/Mutation标记方法returns Type解决异步返回类型的反射限制参数少量参数用Arg内联多参数用ArgsType参数类 解构支持defaultValue、nullable 与 class-validator 校验输入InputType类 PartialT保证类型安全上下文Ctx()注入自定义 contextField ResolverResolver(of Type)FieldResolverRoot配合ResolverInterfaceT获得编译期检查纯计算字段可直接内联到 object type 类中复杂副作用逻辑则放回 resolver 类以利用 DI 与可测试性。上述代码均为教程用途的示例更多更完整的实战案例可以在仓库的 examples 目录中找到从 simple-usage 起步即可快速上手。赞分享后端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 入门用 TypeScript 类与装饰器构建 GraphQL Schema 与 ResolverTypeGraphQL 入门用 TypeScript 类与装饰器构建 GraphQL Schema 与 Resolver TypeGraphQL 是一个面向后端GraphQLAPI设计TypeGraphQL 入门指南用 TypeScript 类与装饰器声明式构建 GraphQL Schema 与 ResolverTypeGraphQL 入门指南用 TypeScript 类与装饰器声明式构建 GraphQL Schema 与 Resolver 导读 TypeGraphQ后端GraphQLAPI设计创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表