NestJS GraphQL 如何集成?
NestJS GraphQL 集成详解GraphQL 概述GraphQL 是一种用于 API 的查询语言和运行时环境。NestJS 通过 @nestjs/graphql 包提供了完整的 GraphQL 支持,使开发者能够构建灵活、高效的 GraphQL API。安装依赖npm install @nestjs/graphql graphql apollo-server-expressnpm install -D @types/graphql基本配置配置 GraphQL 模块import { Module } from '@nestjs/common';import { GraphQLModule } from '@nestjs/graphql';import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';import { join } from 'path';@Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: join(process.cwd(), 'src/schema.gql'), sortSchema: true, playground: true, introspection: true, context: ({ req }) => ({ req }), }), ],})export class AppModule {}定义 Schema使用装饰器定义 Schemaimport { Field, Int, ObjectType, Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';import { User } from './entities/user.entity';@ObjectType()export class User { @Field(() => ID) id: number; @Field() name: string; @Field() email: string; @Field(() => Int) age: number; @Field() isActive: boolean; @Field() createdAt: Date; @Field() updatedAt: Date;}定义输入类型import { InputType, Field } from '@nestjs/graphql';@InputType()export class CreateUserInput { @Field() name: string; @Field() email: string; @Field() password: string; @Field(() => Int, { nullable: true }) age?: number;}@InputType()export class UpdateUserInput { @Field(() => ID) id: number; @Field({ nullable: true }) name?: string; @Field({ nullable: true }) email?: string; @Field(() => Int, { nullable: true }) age?: number;}创建 Resolver基本 Resolverimport { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';import { UsersService } from './users.service';import { User } from './entities/user.entity';import { CreateUserInput, UpdateUserInput } from './dto/user.input';@Resolver(() => User)export class UsersResolver { constructor(private usersService: UsersService) {} @Query(() => [User]) async users(): Promise<User[]> { return this.usersService.findAll(); } @Query(() => User, { nullable: true }) async user(@Args('id', { type: () => ID }) id: number): Promise<User> { return this.usersService.findOne(id); } @Mutation(() => User) async createUser(@Args('input') input: CreateUserInput): Promise<User> { return this.usersService.create(input); } @Mutation(() => User, { nullable: true }) async updateUser(@Args('input') input: UpdateUserInput): Promise<User> { return this.usersService.update(input.id, input); } @Mutation(() => Boolean) async deleteUser(@Args('id', { type: () => ID }) id: number): Promise<boolean> { return this.usersService.remove(id); }}使用自定义装饰器import { createParamDecorator, ExecutionContext } from '@nestjs/common';export const CurrentUser = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.user; },);// 在 Resolver 中使用@Mutation(() => User)async createUser( @Args('input') input: CreateUserInput, @CurrentUser() user: any,): Promise<User> { return this.usersService.create(input, user.id);}关系和加载一对一关系@ObjectType()export class Profile { @Field(() => ID) id: number; @Field() bio: string; @Field(() => User) user: User;}@ObjectType()export class User { @Field(() => ID) id: number; @Field() name: string; @Field(() => Profile, { nullable: true }) profile?: Profile;}一对多关系@ObjectType()export class Post { @Field(() => ID) id: number; @Field() title: string; @Field() content: string; @Field(() => User) author: User;}@ObjectType()export class User { @Field(() => ID) id: number; @Field() name: string; @Field(() => [Post]) posts: Post[];}多对多关系@ObjectType()export class Tag { @Field(() => ID) id: number; @Field() name: string; @Field(() => [Post]) posts: Post[];}@ObjectType()export class Post { @Field(() => ID) id: number; @Field() title: string; @Field(() => [Tag]) tags: Tag[];}分页基于游标的分页import { ArgsType, Field, Int } from '@nestjs/graphql';@ArgsType()export class PaginationArgs { @Field(() => Int, { nullable: true }) first?: number; @Field(() => String, { nullable: true }) after?: string;}@ObjectType()export class PaginatedUsers { @Field(() => [User]) data: User[]; @Field(() => Boolean) hasNextPage: boolean; @Field(() => String, { nullable: true }) endCursor?: string;}@Resolver(() => User)export class UsersResolver { @Query(() => PaginatedUsers) async users(@Args() pagination: PaginationArgs): Promise<PaginatedUsers> { const { data, hasNextPage, endCursor } = await this.usersService.paginate(pagination); return { data, hasNextPage, endCursor }; }}基于偏移量的分页@ArgsType()export class OffsetPaginationArgs { @Field(() => Int, { nullable: true, defaultValue: 0 }) skip?: number; @Field(() => Int, { nullable: true, defaultValue: 10 }) take?: number;}@ObjectType()export class OffsetPaginatedUsers { @Field(() => [User]) data: User[]; @Field(() => Int) total: number; @Field(() => Int) page: number; @Field(() => Int) totalPages: number;}@Resolver(() => User)export class UsersResolver { @Query(() => OffsetPaginatedUsers) async users(@Args() pagination: OffsetPaginationArgs): Promise<OffsetPaginatedUsers> { const { data, total, page, totalPages } = await this.usersService.paginate(pagination); return { data, total, page, totalPages }; }}订阅(Subscriptions)配置订阅import { PubSub } from 'graphql-subscriptions';@Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, installSubscriptionHandlers: true, context: ({ req, connection }) => { if (connection) { return { req: connection.context }; } return { req }; }, }), ],})export class AppModule {}创建订阅 Resolverimport { Resolver, Subscription, Root } from '@nestjs/graphql';import { PubSub } from 'graphql-subscriptions';@ObjectType()export class Message { @Field(() => ID) id: number; @Field() content: string; @Field() createdAt: Date;}@Resolver(() => Message)export class MessagesResolver { private pubSub: PubSub; constructor() { this.pubSub = new PubSub(); } @Subscription(() => Message, { filter: (payload, variables) => { return payload.chatId === variables.chatId; }, }) messageAdded(@Root() message: Message, @Args('chatId') chatId: number): Message { return message; } async publishMessage(message: Message) { await this.pubSub.publish('messageAdded', { messageAdded: message }); }}数据加载器(DataLoader)创建 DataLoaderimport { DataLoader } from 'dataloader';import { UsersService } from './users.service';export class UserDataLoader { private loader: DataLoader<number, User>; constructor(private usersService: UsersService) { this.loader = new DataLoader(async (ids) => { const users = await this.usersService.findByIds(ids); return ids.map(id => users.find(user => user.id === id)); }); } async load(id: number): Promise<User> { return this.loader.load(id); } async loadMany(ids: number[]): Promise<User[]> { return this.loader.loadMany(ids); }}在 Resolver 中使用 DataLoaderimport { Resolver, Query, Args, ID, Parent } from '@nestjs/graphql';import { UserDataLoader } from './user-dataloader';@Resolver(() => Post)export class PostsResolver { constructor(private userDataLoader: UserDataLoader) {} @Query(() => [Post]) async posts(): Promise<Post[]> { return this.postsService.findAll(); } @FieldResolver(() => User) async author(@Parent() post: Post): Promise<User> { return this.userDataLoader.load(post.authorId); }}验证和转换使用 class-validatorimport { IsEmail, IsString, MinLength } from 'class-validator';@InputType()export class CreateUserInput { @Field() @IsEmail() email: string; @Field() @IsString() @MinLength(6) password: string; @Field() @IsString() name: string;}自定义验证器import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';export function IsStrongPassword(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ name: 'isStrongPassword', target: object.constructor, propertyName: propertyName, options: validationOptions, validator: { validate(value: any) { const hasUpperCase = /[A-Z]/.test(value); const hasLowerCase = /[a-z]/.test(value); const hasNumber = /[0-9]/.test(value); return hasUpperCase && hasLowerCase && hasNumber; }, defaultMessage(args: ValidationArguments) { return 'Password must contain uppercase, lowercase, and numbers'; }, }, }); };}@InputType()export class CreateUserInput { @Field() @IsStrongPassword() password: string;}错误处理自定义错误类import { ApolloError } from 'apollo-server-express';export class UserNotFoundError extends ApolloError { constructor(id: number) { super(`User with ID ${id} not found`, 'USER_NOT_FOUND', { id, }); }}export class ValidationError extends ApolloError { constructor(message: string, fields: string[]) { super(message, 'VALIDATION_ERROR', { fields }); }}在 Resolver 中使用错误@Resolver(() => User)export class UsersResolver { constructor(private usersService: UsersService) {} @Query(() => User, { nullable: true }) async user(@Args('id', { type: () => ID }) id: number): Promise<User> { const user = await this.usersService.findOne(id); if (!user) { throw new UserNotFoundError(id); } return user; } @Mutation(() => User) async createUser(@Args('input') input: CreateUserInput): Promise<User> { try { return await this.usersService.create(input); } catch (error) { throw new ValidationError('Invalid input', error.fields); } }}权限控制创建权限守卫import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';import { GqlExecutionContext } from '@nestjs/graphql';@Injectable()export class GqlAuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const ctx = GqlExecutionContext.create(context); const { req } = ctx.getContext(); if (!req.user) { throw new UnauthorizedException(); } return true; }}使用权限守卫import { Resolver, Mutation, UseGuards } from '@nestjs/graphql';import { GqlAuthGuard } from './guards/gql-auth.guard';@Resolver(() => User)export class UsersResolver { @Mutation(() => User) @UseGuards(GqlAuthGuard) async createUser(@Args('input') input: CreateUserInput): Promise<User> { return this.usersService.create(input); }}性能优化查询复杂度分析import { GraphQLModule } from '@nestjs/graphql';import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, validationRules: [ queryComplexity({ maximumComplexity: 1000, variables: {}, onComplete: (complexity) => { console.log(`Query complexity: ${complexity}`); }, }), ],})查询深度限制import { depthLimit } from 'graphql-depth-limit';GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, validationRules: [ depthLimit(5), ],})最佳实践Schema 优先:优先使用 Schema 优先的方法类型安全:充分利用 TypeScript 的类型系统分页:始终实现分页以避免大量数据传输数据加载器:使用 DataLoader 解决 N+1 查询问题错误处理:提供清晰的错误信息权限控制:实现适当的权限控制机制性能监控:监控查询性能和复杂度文档化:使用 GraphQL Playground 和文档工具总结NestJS GraphQL 集成提供了:完整的 GraphQL 支持灵活的 Schema 定义强大的类型安全丰富的查询功能易于集成的生态系统掌握 NestJS GraphQL 集成是构建现代、灵活 API 的关键。通过合理使用 GraphQL 的特性,可以构建出高效、类型安全、易于维护的 API,满足前端对数据的精确需求。GraphQL 的查询语言特性使客户端能够精确获取所需数据,减少网络传输和提升性能。