覆盖文档:GraphQL(Quick Start, Resolvers, Mutations, Subscriptions, Scalars, Directives, Interfaces, Unions/Enums, Field Middleware, Mapped Types, Plugins, Complexity, Extensions, CLI Plugin, Generating SDL, Sharing Models, Other Features, Federation) 前置知识:第9课 源码重点:
@nestjs/graphql的forRoot()动态模块, Resolver 发现机制,GraphQLSchemaHost
一、GraphQL 入门
[基础] 本节面向首次接触 NestJS GraphQL 的读者。
1.1 什么是 GraphQL
GraphQL 是一种 API 查询语言,客户端可以精确指定需要的数据,避免 REST 的过度获取(over-fetching)和不足获取(under-fetching)。
┌──────────────────────────────────────────────────────┐
│ REST │
│ │
│ GET /users/1 → { id, name, email, ... } │
│ GET /users/1/posts → [{ id, title, body, ... }] │
│ 两次请求,返回大量不需要的字段 │
│ │
├──────────────────────────────────────────────────────┤
│ GraphQL │
│ │
│ query { │
│ user(id: 1) { │
│ name │
│ posts { title } │
│ } │
│ } │
│ 一次请求,只返回客户端需要的字段 │
└──────────────────────────────────────────────────────┘
1.2 Code First vs Schema First
NestJS 支持两种 GraphQL 开发模式:
| 模式 | 流程 | 推荐场景 |
|---|---|---|
| Code First(推荐) | TypeScript 类 + 装饰器 → 自动生成 SDL | 与 NestJS 深度集成,装饰器驱动 |
| Schema First | 先写 .graphql 文件 → 生成 TypeScript 类型 | 团队中 GraphQL schema 优先设计 |
本课以 Code First 为主线讲解。
1.3 安装与配置
# Apollo 驱动(主流)
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
# 或 Mercurius 驱动(Fastify 原生)
npm i @nestjs/graphql @nestjs/mercurius mercurius graphql
// app.module.ts — 配置 GraphQL
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>(ApolloDriver, {
// Code First:自动生成 schema 文件
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
// 或 autoSchemaFile: true → 内存中生成,不写入文件
// 可选配置
sortSchema: true, // 字段按字母排序
playground: true, // 启用 Playground(开发环境)
// installSubscriptionHandlers: true, // 启用 Subscription(已废弃,用 subscriptions 配置)
}),
CatsModule,
],
})
export class AppModule {}
1.4 定义 GraphQL 类型
使用 @ObjectType() 和 @Field() 装饰器定义 GraphQL 对象类型:
import { ObjectType, Field, Int, ID } from '@nestjs/graphql';
@ObjectType()
export class Cat {
@Field(() => ID)
id: string;
@Field() // String 类型自动推断
name: string;
@Field(() => Int) // 显式指定 GraphQL 类型
age: number;
@Field(() => String, { nullable: true }) // 可空字段
breed?: string;
@Field(() => [String]) // 数组类型
hobbies: string[];
}
GraphQL 类型映射:
| TypeScript | GraphQL | 装饰器 |
|---|---|---|
string | String | @Field() 自动推断 |
boolean | Boolean | @Field() 自动推断 |
number | Float(默认) | @Field(() => Float) |
number | Int | @Field(() => Int) |
string | ID | @Field(() => ID) |
Date | DateTime | @Field() |
T[] | [T] | @Field(() => [T]) |
1.5 编写 Resolver
Resolver 是 GraphQL 中类似 Controller 的角色:
import {
Resolver,
Query,
Mutation,
Args,
ResolveField,
Parent,
Int,
} from '@nestjs/graphql';
import { Cat } from './cat.model';
import { CatsService } from './cats.service';
import { CreateCatInput } from './dto/create-cat.input';
@Resolver(() => Cat)
export class CatsResolver {
constructor(private readonly catsService: CatsService) {}
// Query:查询所有猫
@Query(() => [Cat], { name: 'cats' })
findAll() {
return this.catsService.findAll();
}
// Query:按 ID 查询
@Query(() => Cat, { name: 'cat' })
findOne(@Args('id', { type: () => Int }) id: number) {
return this.catsService.findOne(id);
}
// Mutation:创建猫
@Mutation(() => Cat)
createCat(@Args('createCatInput') createCatInput: CreateCatInput) {
return this.catsService.create(createCatInput);
}
// ResolveField:解析关联字段(计算属性)
@ResolveField('fullName', () => String)
getFullName(@Parent() cat: Cat) {
return `${cat.name} the ${cat.breed || 'Unknown'}`;
}
}
1.6 输入类型
import { InputType, Field, Int } from '@nestjs/graphql';
@InputType()
export class CreateCatInput {
@Field()
name: string;
@Field(() => Int)
age: number;
@Field({ nullable: true })
breed?: string;
}
@InputType() 对应 GraphQL 的 input 类型,用于 Mutation 和 Query 的参数。@ObjectType() 对应 type,用于返回值。两者不能互换。
1.7 @Args 装饰器
// 简单参数
@Query(() => Cat)
cat(@Args('id', { type: () => Int }) id: number) { ... }
// 对象参数
@Mutation(() => Cat)
createCat(@Args('input') input: CreateCatInput) { ... }
// 多个简单参数
@Query(() => [Cat])
cats(
@Args('skip', { type: () => Int, defaultValue: 0 }) skip: number,
@Args('take', { type: () => Int, defaultValue: 25 }) take: number,
) { ... }
// ArgsType — 将多个参数提取为独立类
@ArgsType()
class GetCatsArgs {
@Field(() => Int, { defaultValue: 0 })
skip: number;
@Field(() => Int, { defaultValue: 25 })
take: number;
}
@Query(() => [Cat])
cats(@Args() args: GetCatsArgs) { ... }
二、类型系统
[中阶] 本节面向已掌握基础 GraphQL 开发的读者。
2.1 自定义标量类型
GraphQL 内置标量:Int、Float、String、Boolean、ID。NestJS 额外提供 GraphQLISODateTime 和 GraphQLTimestamp。
当内置标量不够用时,可以自定义:
import { Scalar, CustomScalar } from '@nestjs/graphql';
import { Kind, ValueNode } from 'graphql';
@Scalar('Date', () => Date)
export class DateScalar implements CustomScalar<number, Date> {
description = 'Date custom scalar type';
// 从客户端接收值时(变量)
parseValue(value: number): Date {
return new Date(value);
}
// 返回给客户端时
serialize(value: Date): number {
return value.getTime();
}
// 从查询字符串中解析字面量
parseLiteral(ast: ValueNode): Date {
if (ast.kind === Kind.INT) {
return new Date(ast.value);
}
return null;
}
}
在模块中注册:
@Module({
providers: [DateScalar], // 注册为 provider
})
export class CommonModule {}
2.2 接口类型
import { InterfaceType, Field, ID } from '@nestjs/graphql';
@InterfaceType({
// 运行时类型判断:根据实际对象决定返回哪个具体类型
resolveType(value) {
if (value.breed) {
return Cat;
}
if (value.tricks) {
return Dog;
}
return null;
},
})
export abstract class Pet {
@Field(() => ID)
id: string;
@Field()
name: string;
}
// 实现接口
@ObjectType({ implements: () => [Pet] })
export class Cat extends Pet {
@Field()
breed: string;
}
@ObjectType({ implements: () => [Pet] })
export class Dog extends Pet {
@Field(() => [String])
tricks: string[];
}
// Resolver 中使用
@Query(() => [Pet])
pets(): Pet[] {
return this.petsService.findAll();
}
2.3 联合类型
import { createUnionType } from '@nestjs/graphql';
export const SearchResultUnion = createUnionType({
name: 'SearchResult',
types: () => [Cat, Dog] as const,
// resolveType 也可以在这里定义
resolveType(value) {
if (value.breed) return Cat;
if (value.tricks) return Dog;
return null;
},
});
// 使用
@Query(() => [SearchResultUnion])
search(@Args('term') term: string) {
return this.searchService.search(term);
}
2.4 枚举类型
import { registerEnumType } from '@nestjs/graphql';
export enum Role {
ADMIN = 'ADMIN',
USER = 'USER',
MODERATOR = 'MODERATOR',
}
registerEnumType(Role, {
name: 'Role',
description: 'User role in the system',
valuesMap: {
ADMIN: { description: 'System administrator' },
MODERATOR: { deprecationReason: 'Use ADMIN instead' },
},
});
// 使用
@ObjectType()
export class User {
@Field(() => Role)
role: Role;
}
2.5 映射类型
与 @nestjs/mapped-types(REST)类似,@nestjs/graphql 提供 GraphQL 版映射类型:
import {
PartialType,
PickType,
OmitType,
IntersectionType,
} from '@nestjs/graphql';
// PartialType:所有字段变可选
@InputType()
export class UpdateCatInput extends PartialType(CreateCatInput) {}
// PickType:只保留指定字段
@InputType()
export class CatNameInput extends PickType(CreateCatInput, ['name'] as const) {}
// OmitType:排除指定字段
@InputType()
export class CatWithoutAgeInput extends OmitType(CreateCatInput, ['age'] as const) {}
// IntersectionType:合并两个类型
@InputType()
export class CatWithOwnerInput extends IntersectionType(
CreateCatInput,
CreateOwnerInput,
) {}
注意:GraphQL 的映射类型来自 @nestjs/graphql,不是 @nestjs/mapped-types(后者用于 REST DTO)。
三、订阅与中间件
[中阶] 本节面向需要实现实时推送和请求拦截的开发者。
3.1 Subscriptions
GraphQL Subscription 用于服务端向客户端实时推送数据(基于 WebSocket):
npm i graphql-ws
// app.module.ts — 启用 Subscription
GraphQLModule.forRoot<ApolloDriverConfig>(ApolloDriver, {
autoSchemaFile: true,
subscriptions: {
'graphql-ws': true, // 推荐:graphql-ws 协议
// 'subscriptions-transport-ws': true, // 废弃协议,兼容旧客户端
},
})
import { PubSub } from 'graphql-subscriptions';
const pubSub = new PubSub(); // 开发环境用内存 PubSub
@Resolver(() => Cat)
export class CatsResolver {
// Mutation:创建猫并发布事件
@Mutation(() => Cat)
async createCat(@Args('input') input: CreateCatInput) {
const cat = await this.catsService.create(input);
// 发布事件
pubSub.publish('catAdded', { catAdded: cat });
return cat;
}
// Subscription:监听新增猫事件
@Subscription(() => Cat, {
// filter:筛选推送目标(按条件过滤)
filter(this: CatsResolver, payload, variables) {
// 只推送指定品种的猫
return payload.catAdded.breed === variables.breed;
},
// resolve:修改推送数据
resolve(payload) {
return payload.catAdded;
},
})
catAdded(@Args('breed', { nullable: true }) breed?: string) {
return pubSub.asyncIterableIterator('catAdded');
}
}
生产环境 PubSub
内存 PubSub 不适合生产环境(多实例无法共享)。使用外部 PubSub:
npm i graphql-redis-subscriptions ioredis
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
const pubSub = new RedisPubSub({
publisher: new Redis({ host: 'localhost', port: 6379 }),
subscriber: new Redis({ host: 'localhost', port: 6379 }),
});
3.2 Field Middleware
字段中间件在字段级别拦截解析过程,可以在字段解析前后执行自定义逻辑:
import { FieldMiddleware, MiddlewareContext, NextFn } from '@nestjs/graphql';
// 日志中间件
const loggerMiddleware: FieldMiddleware = async (
ctx: MiddlewareContext,
next: NextFn,
) => {
const value = await next();
console.log(`Field ${ctx.info.fieldName} resolved to:`, value);
return value;
};
// 权限检查中间件
const checkRoleMiddleware: FieldMiddleware = async (
ctx: MiddlewareContext,
next: NextFn,
) => {
const { info } = ctx;
const { extensions } = info.parentType.getFields()[info.fieldName];
const requiredRole = extensions?.role;
if (requiredRole) {
const userRole = ctx.context.req.user?.role;
if (userRole !== requiredRole) {
return null; // 无权限则返回 null
}
}
return next();
};
// 应用到字段
@ObjectType()
export class User {
@Field()
name: string;
@Field({ middleware: [checkRoleMiddleware] })
secretField: string;
}
也可以全局注册字段中间件:
GraphQLModule.forRoot<ApolloDriverConfig>(ApolloDriver, {
autoSchemaFile: true,
buildSchemaOptions: {
fieldMiddleware: [loggerMiddleware], // 全局字段中间件
},
})
3.3 Directives(指令)
GraphQL 指令用于在 schema 层面添加自定义行为:
import { Directive, Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Cat {
@Field()
@Directive('@upper') // 自定义指令
name: string;
@Field()
@Directive('@deprecated(reason: "Use fullName instead")') // 内置指令
breed: string;
}
实现自定义指令需要使用 mapSchema 进行 schema 转换:
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { defaultFieldResolver, GraphQLSchema } from 'graphql';
function upperDirectiveTransformer(
schema: GraphQLSchema,
directiveName: string,
) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const upperDirective = getDirective(
schema,
fieldConfig,
directiveName,
)?.[0];
if (upperDirective) {
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async function (source, args, context, info) {
const result = await resolve(source, args, context, info);
if (typeof result === 'string') {
return result.toUpperCase();
}
return result;
};
}
return fieldConfig;
},
});
}
// 配置
GraphQLModule.forRoot<ApolloDriverConfig>(ApolloDriver, {
autoSchemaFile: true,
transformSchema: (schema) =>
upperDirectiveTransformer(schema, 'upper'),
buildSchemaOptions: {
directives: [
new GraphQLDirective({
name: 'upper',
locations: [DirectiveLocation.FIELD_DEFINITION],
}),
],
},
})
3.4 Guards / Interceptors / Filters 在 GraphQL 中
GraphQL 中使用 NestJS 增强器时,需要通过 GqlExecutionContext 获取上下文:
import { ExecutionContext, Injectable, CanActivate } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
@Injectable()
export class GqlAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// 将 ExecutionContext 转换为 GqlExecutionContext
const ctx = GqlExecutionContext.create(context);
const gqlContext = ctx.getContext(); // GraphQL 上下文(包含 req)
const args = ctx.getArgs(); // GraphQL 参数
const info = ctx.getInfo(); // GraphQL 解析信息
const root = ctx.getRoot(); // 根值
const request = gqlContext.req;
return !!request.headers.authorization;
}
}
// 使用方式与 HTTP 完全相同
@UseGuards(GqlAuthGuard)
@Query(() => [Cat])
cats() {
return this.catsService.findAll();
}
四、高级特性
[高阶] 本节面向需要深度使用 GraphQL 的高级开发者。
4.1 查询复杂度限制
防止恶意客户端发送超深层嵌套查询耗尽服务器资源:
npm i graphql-query-complexity
import {
fieldExtensionsEstimator,
getComplexity,
simpleEstimator,
} from 'graphql-query-complexity';
import { GraphQLSchemaHost } from '@nestjs/graphql';
import { Plugin } from '@nestjs/apollo';
import {
ApolloServerPlugin,
GraphQLRequestListener,
} from '@apollo/server';
@Plugin()
export class ComplexityPlugin implements ApolloServerPlugin {
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
async requestDidStart(): Promise<GraphQLRequestListener<any>> {
const { schema } = this.gqlSchemaHost;
const maxComplexity = 20; // 最大复杂度阈值
return {
async didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
operationName: request.operationName,
query: document,
variables: request.variables,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
if (complexity > maxComplexity) {
throw new Error(
`Query too complex: ${complexity}. Maximum: ${maxComplexity}`,
);
}
},
};
}
}
为特定字段设置复杂度:
@ObjectType()
export class Cat {
@Field()
name: string;
// friends 字段复杂度 = 10
@Field(() => [Cat], { complexity: 10 })
friends: Cat[];
// 动态复杂度:基于请求的 limit 参数
@Field(() => [Cat], {
complexity: (options) => options.args.limit * options.childComplexity,
})
relatedCats: Cat[];
}
4.2 Extensions
@Extensions() 为字段添加自定义元数据,配合 Field Middleware 实现细粒度控制:
import { Extensions, Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field()
name: string;
@Field()
@Extensions({ role: Role.ADMIN }) // 只有 ADMIN 可见
secretInfo: string;
@Field()
@Extensions({ deprecated: true, replacedBy: 'fullAddress' })
address: string;
}
配合 Field Middleware 读取并使用这些元数据:
const roleCheckMiddleware: FieldMiddleware = async (ctx, next) => {
const { extensions } = ctx.info.parentType.getFields()[ctx.info.fieldName];
if (extensions?.role) {
const user = ctx.context.req.user;
if (user?.role !== extensions.role) {
return null;
}
}
return next();
};
4.3 Plugins(Apollo 插件)
实现 ApolloServerPlugin 接口,在 Apollo 请求生命周期中插入自定义逻辑:
import { Plugin } from '@nestjs/apollo';
import {
ApolloServerPlugin,
GraphQLRequestListener,
} from '@apollo/server';
@Plugin()
export class LoggingPlugin implements ApolloServerPlugin {
async requestDidStart(requestContext): Promise<GraphQLRequestListener<any>> {
const start = Date.now();
console.log('Request started');
return {
async willSendResponse() {
console.log(`Request completed in ${Date.now() - start}ms`);
},
async didEncounterErrors({ errors }) {
console.error('GraphQL errors:', errors);
},
};
}
}
@Plugin() 标记的类会被自动注册到 Apollo Server。
4.4 CLI Plugin
CLI Plugin 通过编译时自动添加 @Field() 装饰器,减少大量样板代码:
// nest-cli.json
{
"compilerOptions": {
"plugins": [
{
"name": "@nestjs/graphql",
"options": {
"typeFileNameSuffix": [".input.ts", ".args.ts", ".entity.ts", ".model.ts"],
"introspectComments": true
}
}
]
}
}
使用前(手动添加所有 @Field()):
@ObjectType()
export class Cat {
@Field(() => ID)
id: string;
@Field()
name: string;
@Field(() => Int)
age: number;
@Field({ nullable: true })
breed?: string;
}
使用后(Plugin 自动推断):
@ObjectType()
export class Cat {
id: string; // 自动推断为 String
name: string; // 自动推断为 String
age: number; // 自动推断为 Float(注意:不会自动推断 Int)
breed?: string; // 自动推断为 nullable String
}
注意:CLI Plugin 只能推断 TypeScript 类型到 GraphQL 类型,对于 Int / ID 等无法从 TypeScript 类型区分的标量仍需手动标注。
4.5 前后端模型共享
在前端项目中复用后端的 @ObjectType 类时,避免引入 @nestjs/graphql 整个包:
// webpack.config.js(前端项目)
module.exports = {
resolve: {
alias: {
'@nestjs/graphql': '@nestjs/graphql/dist/extra/graphql-model-shim',
},
},
};
graphql-model-shim 将所有装饰器替换为空操作(no-op),避免前端项目报错。
4.6 SDL 生成
离线生成 GraphQL Schema 文件(用于工具链或文档):
import { GraphQLSchemaBuilderModule, GraphQLSchemaFactory } from '@nestjs/graphql';
import { NestFactory } from '@nestjs/core';
async function generateSchema() {
const app = await NestFactory.create(GraphQLSchemaBuilderModule);
await app.init();
const gqlSchemaFactory = app.get(GraphQLSchemaFactory);
const schema = await gqlSchemaFactory.create([CatsResolver]);
// 使用 graphql 的 printSchema 输出 SDL
const { printSchema } = require('graphql');
console.log(printSchema(schema));
await app.close();
}
generateSchema();
五、Federation 微服务联邦
[高阶] 本节面向需要构建分布式 GraphQL 架构的开发者。
5.1 什么是 Apollo Federation
Apollo Federation 允许将 GraphQL schema 拆分到多个独立服务(Subgraph),通过 Gateway 统一对外暴露:
┌─────────────────────────────────────┐
│ Apollo Gateway │
│ (统一 GraphQL 端点) │
├──────────┬───────────┬──────────────┤
│ │ │ │
▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐
│Users │ │Posts │ │Comments│ │Products │
│Subgraph│ │Subgraph│ │Subgraph│ │Subgraph │
└──────┘ └──────┘ └──────┘ └──────────┘
5.2 Subgraph 服务
npm i @nestjs/apollo @apollo/subgraph
// users.module.ts — Users Subgraph
import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo';
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>(
ApolloFederationDriver,
{
autoSchemaFile: {
federation: 2, // Federation v2
},
},
),
],
})
export class AppModule {}
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
// @key 指令标记实体主键(Federation 核心概念)
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field(() => ID)
id: string;
@Field()
name: string;
@Field()
email: string;
}
import { Resolver, Query, ResolveReference } from '@nestjs/graphql';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Query(() => [User])
users() {
return this.usersService.findAll();
}
// 当其他 Subgraph 引用 User 实体时,通过此方法解析完整对象
@ResolveReference()
resolveReference(reference: { __typename: string; id: string }) {
return this.usersService.findOne(reference.id);
}
}
跨服务引用
在 Posts Subgraph 中引用 Users 的 User 实体:
// Posts Subgraph 中的 User(只声明 @key 字段)
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field(() => ID)
id: string;
}
@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field(() => ID)
id: string;
@Field()
title: string;
@Field(() => User)
author: User; // 引用 Users Subgraph 的实体
}
5.3 Gateway 服务
npm i @nestjs/apollo @apollo/gateway @apollo/server
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { IntrospectAndCompose } from '@apollo/gateway';
@Module({
imports: [
GraphQLModule.forRoot<ApolloGatewayDriverConfig>(ApolloGatewayDriver, {
gateway: {
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:3001/graphql' },
{ name: 'posts', url: 'http://localhost:3002/graphql' },
],
}),
},
}),
],
})
export class AppModule {}
5.4 Federation 限制
| 特性 | 支持情况 |
|---|---|
| Query | 完全支持 |
| Mutation | 完全支持 |
| Subscription | 不支持(Federation 架构限制) |
| 跨服务实体引用 | 支持(@key + @ResolveReference) |
| 接口/联合类型 | 支持(需在 Subgraph 中完整定义) |
六、源码解读:GraphQL 模块
[资深] 本节面向希望深入理解 GraphQL 模块内部机制的读者。
6.1 forRoot() 动态模块
GraphQLModule.forRoot() 是一个动态模块,负责:
// @nestjs/graphql 内部流程
GraphQLModule.forRoot(ApolloDriver, options)
│
├─→ 1. 创建 DynamicModule
│ 注册 ApolloDriver 为 GQL_DRIVER provider
│
├─→ 2. DiscoveryService 扫描所有 @Resolver() 类
│ 收集 @Query / @Mutation / @Subscription / @ResolveField
│
├─→ 3. 根据 Code First / Schema First 生成 GraphQLSchema
│ - Code First:TypeScript 装饰器 → SDL → Schema
│ - Schema First:.graphql 文件 → Schema
│
├─→ 4. 将 Schema 传给 Driver(Apollo / Mercurius)
│
└─→ 5. Driver 创建 HTTP 中间件并挂载到 NestJS
6.2 Resolver 发现机制
NestJS 使用 DiscoveryService(来自 @nestjs/core)扫描所有被 @Resolver() 装饰的类:
@Resolver(() => Cat) ← Metadata: GQL_RESOLVER_METADATA
│
├── @Query(() => [Cat]) ← Metadata: GQL_QUERY_METADATA
├── @Mutation(() => Cat) ← Metadata: GQL_MUTATION_METADATA
├── @Subscription(() => Cat) ← Metadata: GQL_SUBSCRIPTION_METADATA
└── @ResolveField('fullName') ← Metadata: GQL_RESOLVE_FIELD_METADATA
扫描过程:
ResolversExplorerService通过DiscoveryService找到所有标记了@Resolver()的 provider- 使用
MetadataScanner扫描每个 resolver 类的方法 - 读取方法上的装饰器元数据,构建 resolver map
- 将 resolver map 传给 schema 生成器
6.3 GraphQLSchemaHost
GraphQLSchemaHost 提供运行时访问当前 GraphQL schema 的能力:
import { GraphQLSchemaHost } from '@nestjs/graphql';
@Injectable()
export class SchemaInspector {
constructor(private readonly schemaHost: GraphQLSchemaHost) {}
inspectSchema() {
const { schema } = this.schemaHost;
// 获取所有类型
const typeMap = schema.getTypeMap();
// 获取 Query 类型
const queryType = schema.getQueryType();
// 获取所有字段
const fields = queryType.getFields();
}
}
GraphQLSchemaHost 内部很简单,就是持有一个 GraphQLSchema 实例的引用,在模块初始化完成后由 Driver 设置。
七、GraphQL 架构策略
[架构] 本节面向技术负责人和架构师。
7.1 REST vs GraphQL 选型
| 维度 | REST | GraphQL |
|---|---|---|
| 数据获取 | 固定端点,固定返回结构 | 客户端按需查询 |
| Over-fetching | 常见(返回不需要的字段) | 无(只返回请求的字段) |
| Under-fetching | 需多次请求拼装 | 单次请求获取所有需要的数据 |
| 缓存 | HTTP 缓存(URL 级别)简单 | 需要专门的缓存策略 |
| 文件上传 | 原生支持(multipart) | 需要额外处理 |
| 版本管理 | URL 版本(/v1、/v2) | Schema 演进(@deprecated) |
| 监控 | 按 URL 区分,直观 | 所有请求走同一端点,需按 operation 区分 |
| 适用场景 | 简单 CRUD、开放 API、移动端 | 复杂前端、BFF 层、多客户端差异查询 |
7.2 Schema 设计原则
- 客户端驱动:Schema 应面向客户端需求设计,而非直接映射数据库表结构
- 避免过度嵌套:关联深度控制在 3-4 层以内
- 使用 Connection 分页:Relay-style cursor 分页优于 offset 分页
- 合理使用 nullable:只有真正可能为 null 的字段才标记为 nullable
- Mutation 返回完整对象:方便客户端缓存更新
- Input 类型与 Object 类型分离:不要复用
// 推荐的分页设计
@ObjectType()
export class CatConnection {
@Field(() => [CatEdge])
edges: CatEdge[];
@Field(() => PageInfo)
pageInfo: PageInfo;
@Field(() => Int)
totalCount: number;
}
@ObjectType()
export class CatEdge {
@Field(() => Cat)
node: Cat;
@Field()
cursor: string;
}
@ObjectType()
export class PageInfo {
@Field()
hasNextPage: boolean;
@Field()
hasPreviousPage: boolean;
@Field({ nullable: true })
startCursor?: string;
@Field({ nullable: true })
endCursor?: string;
}
7.3 N+1 问题与 DataLoader
GraphQL 最常见的性能问题——N+1 查询:
# 查询 10 只猫和它们的主人
query {
cats { # 1 次查询:获取 10 只猫
name
owner { # 10 次查询:每只猫查一次 owner
name
}
}
}
# 总计 11 次数据库查询!
解决方案——DataLoader 批量加载:
npm i dataloader
import DataLoader from 'dataloader';
@Injectable({ scope: Scope.REQUEST }) // 必须 REQUEST 作用域
export class OwnersLoader {
constructor(private readonly ownersService: OwnersService) {}
// batchLoadFn:一次性加载多个 owner
public readonly batchOwners = new DataLoader<string, Owner>(
async (ownerIds: readonly string[]) => {
const owners = await this.ownersService.findByIds([...ownerIds]);
// 保证返回顺序与输入 id 顺序一致
const ownersMap = new Map(owners.map((o) => [o.id, o]));
return ownerIds.map((id) => ownersMap.get(id));
},
);
}
@Resolver(() => Cat)
export class CatsResolver {
constructor(private readonly ownersLoader: OwnersLoader) {}
@ResolveField('owner', () => Owner)
getOwner(@Parent() cat: Cat) {
// DataLoader 自动批量化:10 次 load 合并为 1 次 batchLoadFn
return this.ownersLoader.batchOwners.load(cat.ownerId);
}
}
结果:11 次查询 → 2 次查询(1 次猫 + 1 次批量 owner)。
7.4 Federation vs Schema Stitching
| 维度 | Apollo Federation | Schema Stitching |
|---|---|---|
| 维护方 | Apollo(官方推荐) | 社区(graphql-tools) |
| 架构 | Subgraph + Gateway | 多个 schema 拼接 |
| 实体引用 | @key + @ResolveReference | 自定义合并逻辑 |
| 类型扩展 | 原生支持(@extends) | 需手动处理 |
| 生态工具 | Apollo Studio、Rover CLI | 较少 |
| 推荐度 | 首选 | 仅在特殊场景使用 |
7.5 GraphQL 安全检查清单
| 检查项 | 方案 |
|---|---|
| 查询深度限制 | depthLimit 插件 |
| 查询复杂度限制 | graphql-query-complexity |
| 速率限制 | @nestjs/throttler(配合 GqlThrottlerGuard) |
| 认证授权 | Guard + @Extensions({ role }) |
| 字段级权限 | Field Middleware + Extensions |
| 禁用 Introspection | 生产环境 introspection: false |
| 请求大小限制 | Apollo Server csrfPrevention + body parser limit |
八、课后实践
练习 1:GraphQL Code First 入门(基础)
为 CatsModule 实现 GraphQL 版本:
// 1. 安装依赖
// npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
// 2. 创建 Cat ObjectType
@ObjectType()
export class Cat {
@Field(() => ID)
id: string;
@Field()
name: string;
@Field(() => Int)
age: number;
}
// 3. 创建 CreateCatInput
@InputType()
export class CreateCatInput {
@Field()
name: string;
@Field(() => Int)
age: number;
}
// 4. 创建 CatsResolver,实现 cats Query 和 createCat Mutation
// 5. 访问 http://localhost:3000/graphql 在 Playground 中测试
练习 2:实现 Subscription(中阶)
// 1. 安装 graphql-ws
// npm i graphql-ws
// 2. 配置 subscriptions: { 'graphql-ws': true }
// 3. 创建 PubSub 实例
// 4. 在 createCat Mutation 中 pubSub.publish('catAdded', ...)
// 5. 创建 @Subscription catAdded
// 6. 在 Playground 中打开两个标签页:
// 标签 1:subscription { catAdded { id name } }
// 标签 2:mutation { createCat(input: { name: "Tom", age: 3 }) { id } }
练习 3:查询复杂度限制(高阶)
// 1. 安装 graphql-query-complexity
// npm i graphql-query-complexity
// 2. 实现 ComplexityPlugin
// 3. 为 friends 字段设置 complexity: 10
// 4. 发送一个超过复杂度阈值的查询,验证被拦截
练习 4:阅读 @nestjs/graphql 源码(资深)
克隆 @nestjs/graphql 仓库,阅读以下文件:
packages/graphql/lib/graphql.module.ts—forRoot()动态模块packages/graphql/lib/services/resolvers-explorer.service.ts— Resolver 发现packages/graphql/lib/graphql-schema.host.ts— SchemaHost 实现
回答:
autoSchemaFile是如何触发 schema 自动生成的?@Resolver()装饰器注册了什么元数据?- Schema First 模式下,类型定义文件是怎么生成的?
九、本课知识点总结
| 知识点 | 要点 |
|---|---|
| 两种模式 | Code First(装饰器生成 SDL,推荐)vs Schema First(先写 .graphql 文件) |
| 两种驱动 | Apollo(主流)vs Mercurius(Fastify 原生) |
| 核心装饰器 | @ObjectType + @Field、@Resolver + @Query + @Mutation + @ResolveField、@Args + @InputType |
| 类型系统 | @Scalar(自定义标量)、@InterfaceType、createUnionType、registerEnumType、映射类型(Partial/Pick/Omit/Intersection) |
| 订阅 | PubSub + @Subscription + asyncIterableIterator,生产用 Redis PubSub |
| 字段中间件 | @Field({ middleware }) 实现字段级 AOP |
| 复杂度控制 | graphql-query-complexity 防止恶意深查询 |
| CLI Plugin | 编译时自动添加 @Field(),减少 80% 样板代码 |
| Federation | ApolloFederationDriver(Subgraph)+ ApolloGatewayDriver(Gateway),不支持 Subscription |
| N+1 问题 | DataLoader 批量加载,11 次查询 → 2 次 |
| 源码入口 | GraphQLModule.forRoot() → DiscoveryService 扫描 Resolver → Driver 创建中间件 |
下一课预告:第十九课将学习 Standalone Application 和 CRON 任务,掌握 NestJS 在非 HTTP 场景下的应用,包括命令行工具、定时任务、Worker 进程等。