TypeGraphQL 自定义装饰器(Custom Decorators)完整实战指南:方法、类与参数三种模式及底层原理
TypeGraphQL 自定义装饰器(Custom Decorators)完整实战指南:方法、类与参数三种模式及底层原理
导读
在 TypeGraphQL 中,@Query、@Mutation、@Arg、@Ctx 等内置装饰器帮助我们用类与装饰器的方式声明式地构建 GraphQL schema 与 resolver。但当多个 resolver 之间存在重复的校验、鉴权、参数注入等逻辑时,就会产生大量样板代码。TypeGraphQL 提供了一整套自定义装饰器机制——包括方法装饰器(Method Decorators)、Resolver 类装饰器(Resolver Class Decorators)和参数装饰器(Parameter Decorators)——让我们可以把通用逻辑封装成语义化、可复用的装饰器,直接应用于字段、整个 Resolver 类或单个方法参数。读完本文,你将掌握三种自定义装饰器的创建方式、参数注入与 @Arg 注册的进阶用法,并通过仓库源码理解其底层实现原理。
为什么需要自定义装饰器
原文档指出,自定义装饰器是"减少样板代码、在 resolver 之间复用通用逻辑"的利器。TypeGraphQL 支持三种自定义装饰器:
| 类型 | 辅助函数 | 返回类型 | 作用范围 |
|---|---|---|---|
| 方法装饰器 | createMethodMiddlewareDecorator |
MethodDecorator |
单个字段/resolver 方法 |
| Resolver 类装饰器 | createResolverClassMiddlewareDecorator |
ClassDecorator |
整个 Resolver 类(作用于该类下所有 Query/Mutation) |
| 参数装饰器 | createParameterDecorator |
ParameterDecorator |
单个方法参数,可向方法注入返回值 |
这三种辅助函数均从 type-graphql 包导出,在仓库中位于 src/decorators 目录,对应源码文件为 createMethodMiddlewareDecorator.ts、createResolverClassMiddlewareDecorator.ts 与 createParameterDecorator.ts。
方法装饰器:用中间件逻辑封装语义化装饰器
方法装饰器本质上是中间件(middleware)的语法糖封装。TypeGraphQL 的中间件机制详见 middlewares.md,它允许复用 resolver 间的公共代码;而自定义方法装饰器在此基础上进一步收敛 API。
创建方式:调用 createMethodMiddlewareDecorator 辅助函数,传入中间件逻辑(MiddlewareFn)并返回其结果:
export function ValidateArgs(schema: JoiSchema) {
return createMethodMiddlewareDecorator(async ({ args }, next) => {
// 中间件代码,可使用自定义装饰器传入的参数
// 例如基于 'joi' schema 的校验逻辑
await joiValidate(schema, args);
return next();
});
}
从源码看,createMethodMiddlewareDecorator 的实现极为简洁——它直接返回内置的 @UseMiddleware 装饰器:
// src/decorators/createMethodMiddlewareDecorator.ts
export function createMethodMiddlewareDecorator<TContextType extends object = object>(
resolver: MiddlewareFn<TContextType>,
): MethodDecorator {
return UseMiddleware(resolver);
}
也就是说,自定义方法装饰器 = @UseMiddleware(中间件函数) 的再封装。中间件签名 MiddlewareFn<TContext> 接收两个参数:action: ResolverData<TContext>(包含 root、args、context、info)与 next: NextFn,定义见 src/typings/middleware.ts。
使用方法:将自定义装饰器放在 resolver/字段上方并传入所需参数,还可以与内置的 @UseMiddleware 混用:
@Resolver()
export class RecipeResolver {
@ValidateArgs(MyArgsSchema) // 自定义装饰器
@UseMiddleware(ResolveTime) // 显式中间件
@Query()
randomValue(@Args() { scale }: MyArgs): number {
return Math.random() * scale;
}
}
注意装饰器的执行顺序:TypeGraphQL 会按装饰器声明顺序(自上而下)收集并执行中间件,即 ValidateArgs 的校验先于 ResolveTime 的计时执行。
Resolver 类装饰器:一次声明,作用于整个类
与方法装饰器类似,我们可以创建作用于整个 Resolver 类的自定义装饰器。此时需要调用 createResolverClassMiddlewareDecorator:
export function ValidateArgs(schema: JoiSchema) {
return createResolverClassMiddlewareDecorator(async ({ args }, next) => {
// 中间件代码,可使用自定义装饰器传入的参数
// 例如基于 'joi' schema 的校验逻辑
await joiValidate(schema, args);
return next();
});
}
其用法与方法装饰器几乎一致,只是装饰器被放置于 Resolver 类之上:
@ValidateArgs(MyArgsSchema) // 自定义装饰器
@UseMiddleware(ResolveTime) // 显式中间件
@Resolver()
export class RecipeResolver {
@Query()
randomValue(@Args() { scale }: MyArgs): number {
return Math.random() * scale;
}
}
这样,我们只需在代码中放置一次,自定义装饰器就会被应用到该 Resolver 的所有 Query 与 Mutation 上。
底层原理同样清晰:createResolverClassMiddlewareDecorator 也只是对 UseMiddleware 的封装,唯一区别是返回类型为 ClassDecorator:
// src/decorators/createResolverClassMiddlewareDecorator.ts
export function createResolverClassMiddlewareDecorator<TContextType extends object = object>(
resolver: MiddlewareFn<TContextType>,
): ClassDecorator {
return UseMiddleware(resolver);
}
@UseMiddleware 本身是一个同时支持类与方法的重载装饰器(UseMiddleware.ts):
- 当
propertyKey为空(装饰在类上)时,调用collectResolverMiddlewareMetadata将该中间件注册到整个 Resolver 类; - 当装饰在方法上时,调用
collectMiddlewareMetadata仅注册到该字段。
因此同一个 @UseMiddleware 才能既被方法装饰器复用,也被类装饰器复用。
参数装饰器:向方法注入返回值
参数装饰器与中间件最大的不同在于:它有能力返回一个值并注入到方法的对应参数中。这有效减少了原先通过污染 context 来在中间件与 resolver 之间通信的变通做法。
参数装饰器可以只是一个简单的数据提取函数,让 resolver 更易于单元测试:
function CurrentUser() {
return createParameterDecorator<MyContextType>(({ context }) => context.currentUser);
}
也可以更高级,封装部分计算逻辑。相比中间件,参数装饰器对代码执行时机有更细粒度的控制——例如只在真正被请求时才基于 GraphQL info 计算字段映射(通过 @Fields() 装饰器触发):
function Fields(level = 1): ParameterDecorator {
return createParameterDecorator(async ({ info }) => {
const fieldsMap: FieldsMap = {};
// 基于 GraphQL 解析器的 'info' 参数与 level 参数
// 计算请求字段的对象信息
// 甚至可以调用异步服务,因为它是普通 async 函数,可以直接 'await'
return fieldsMap;
});
}
性能提醒(原文档强调):自定义参数装饰器的逻辑若是
async函数,可能会拖慢 GraphQL resolver 的执行速度,因此如无必要应尽量使用同步逻辑。
参数装饰器在 resolver 中的用法与内置装饰器(@Args、@Arg、@Ctx 等)完全一致,其返回值会直接作为对应参数的实参:
@Resolver()
export class RecipeResolver {
constructor(private readonly recipesRepository: Repository<Recipe>) {}
@Authorized()
@Mutation(returns => Recipe)
async addRecipe(
@Args() recipeData: AddRecipeInput,
// 自定义装饰器,用法与内置装饰器一致
@CurrentUser() currentUser: User,
) {
const recipe: Recipe = {
...recipeData,
// 在 resolver 代码中使用自定义装饰器返回的数据
author: currentUser,
};
await this.recipesRepository.save(recipe);
return recipe;
}
@Query(returns => Recipe, { nullable: true })
async recipe(
@Arg("id") id: string,
// 从 GraphQL 查询 info 中解析字段的自定义装饰器
@Fields() fields: FieldsMap,
) {
return await this.recipesRepository.find(id, {
// 将字段映射作为 select 投影以优化数据库查询
select: fields,
});
}
}
从源码层面看,createParameterDecorator 返回的装饰器在调用时会将元数据收集到 metadata storage:
// src/decorators/createParameterDecorator.ts
getMetadataStorage().collectHandlerParamMetadata({
kind: "custom",
target: prototype.constructor,
methodName: propertyKey,
index: parameterIndex,
resolver,
options,
});
其中 kind: "custom" 对应的元数据结构为 CustomParamMetadata(见 src/metadata/definitions/param-metadata.ts),它保存了 resolver 函数与可选参数注册选项。最终在 schema 生成阶段,src/resolvers/create.ts 的 createHandlerResolver 中会先执行中间件链,再由 getParams 依次解析各参数的元数据、调用自定义参数 resolver 获取值,最后通过 targetInstance[methodName].apply(targetInstance, params) 调用 resolver 方法——这正是"参数注入"得以实现的底层执行链路。
自定义 @Arg 装饰器:注册并暴露 GraphQL 参数
某些场景下,我们希望自定义装饰器不仅能注入参数值,还能在 GraphQL schema 中注册/暴露一个参数。此时如果在一个自定义装饰器内同时调用 Arg() 与 createParameterDecorator(),会与 TypeGraphQL 内部机制产生冲突。
为此,createParameterDecorator() 支持第二个参数 CustomParameterOptions,其中 arg 键用于提供 @Arg 的元数据,实现"一举两得":
function RandomIdArg(argName = "id") {
return createParameterDecorator(
// 在此实现"取用户提供的参数,或生成随机 id"的逻辑
({ args }) => args[argName] ?? Math.round(Math.random() * MAX_ID_VALUE),
{
// 在此提供元数据,将参数注册为 GraphQL 参数
arg: {
name: argName,
typeFunc: () => Int,
options: {
nullable: true,
description: "Accepts provided id or generates a random one.",
},
},
},
);
}
CustomParameterOptions 的类型定义(arg 选项包含 name、typeFunc 与可选的 options: ArgOptions)见 src/decorators/createParameterDecorator.ts,其中 ArgOptions 合并了类型选项、描述、校验与废弃标记等配置(定义见 src/decorators/Arg.ts)。从源码可以看到,当传入 paramOptions.arg 时,createParameterDecorator 内部会调用与 @Arg 相同的 getParamInfo 辅助函数(src/helpers/params.ts),通过 design:paramtypes 反射元数据或 typeFunc 推断 GraphQL 类型,并把这些信息一并存入 options.arg——这正是自定义 @Arg 装饰器能正确生成 schema 参数的原因。
使用时与普通的 @Arg 装饰器几乎无差别:
@Resolver()
export class RecipeResolver {
constructor(private readonly recipesRepository: Repository<Recipe>) {}
@Query(returns => Recipe, { nullable: true })
async recipe(
// 自定义装饰器:在 schema 中暴露一个 arg
@RandomIdArg("id") id: number,
) {
return await this.recipesRepository.findById(id);
}
}
小提示:
arg.options同样支持传入validateFn自定义校验函数。仓库示例 random-id-arg.ts 中就对生成/传入的 id 做了取值范围校验:validateFn会在值超出[0, MAX_ID_VALUE]时抛出错误。
仓库实战示例:middlewares-custom-decorators
原文档末尾提供了配套示例项目。在仓库中,完整的可运行示例位于 examples/middlewares-custom-decorators,包含三种自定义装饰器的真实实现:
- decorators/validate-args.ts:基于
class-validator的ValidateArgs方法装饰器。它接收一个ClassType<T>,将args实例化后调用validate(),若存在校验错误则抛出ArgumentValidationError,否则继续执行next()。注释明确指出:示例使用class-validator,你也可以换成joi或任意校验库。 - decorators/current-user.ts:极简参数装饰器,从
context中提取currentUser,泛型参数显式标注为Context类型。 - decorators/random-id-arg.ts:自定义
@Arg装饰器的完整实现,包含arg元数据与validateFn校验。 - decorators/index.ts:统一导出上述装饰器。
在 recipe/recipe.resolver.ts 中可以看到它们的组合用法:类级使用 @UseMiddleware(ResolveTimeMiddleware),recipes 查询上叠加 @ValidateArgs(RecipesArgs)(参数类见 recipe.args.ts,包含 @Min(0) 的 skip 与 @Min(1) @Max(50) 的 take),并同时使用 @RandomIdArg("id") 与 @CurrentUser() 两个参数装饰器。运行示例项目即可直观观察装饰器对查询入参校验、执行耗时统计与上下文注入的实际效果。
总结
自定义装饰器是 TypeGraphQL 将"可复用逻辑"与"声明式 API"结合的精华特性:
- 方法装饰器(
createMethodMiddlewareDecorator)把中间件逻辑封装成语义化装饰器,应用于单个字段; - Resolver 类装饰器(
createResolverClassMiddlewareDecorator)把同样的逻辑提升到类级别,一次声明、作用于该类所有 Query/Mutation; - 参数装饰器(
createParameterDecorator)直接向方法参数注入返回值,比中间件更精细地控制执行时机,且天然友好于单元测试; - 自定义
@Arg装饰器借助CustomParameterOptions的arg键,在注入参数值的同时把参数注册进 GraphQL schema,替代"内部同时调用Arg()与createParameterDecorator()"的冲突写法。
从源码看,方法/类装饰器本质都是 @UseMiddleware 的封装(createMethodMiddlewareDecorator.ts、createResolverClassMiddlewareDecorator.ts),而参数装饰器则通过 metadata storage 记录 kind: "custom" 的参数元数据,由 src/resolvers/create.ts 在 resolver 执行前统一解析注入。掌握这一机制后,你可以将校验、鉴权、字段投影、随机参数生成等通用逻辑沉淀为项目内部的"专属 DSL",让 resolver 代码保持干净、可读且易于测试。