- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
中间件(Middleware)是 TypeGraphQL 中一组可复用的代码片段,能够以声明式方式附加到 resolver 与字段上,用于实现日志、鉴权、耗时统计、错误拦截、结果改写等横切关注点。本指南以 v2.0.0-beta.4 版官方文档为主体,结合仓库源码(src/resolvers/helpers.ts、src/resolvers/create.ts、src/decorators/UseMiddleware.ts)与测试用例(tests/functional/middlewares.ts),系统讲解中间件的创建、挂载、全局注册与底层执行原理。读完本文,你将能够编写函数式与类式中间件、构建守卫与错误拦截器、注册全局中间件,并理解其在运行时“洋葱模型”中的真实调用顺序。
什么是中间件:基于 Koa 启发的 next 栈模型
在 TypeGraphQL 中,中间件本质上是一个接收 2 个参数的函数:
- resolver data:与 resolver 收到的数据完全一致,即
root、args、context、info四件套; next函数:用于控制下一个中间件以及所挂载 resolver 的执行。
官方文档明确指出,TypeGraphQL 的中间件灵感来自 koa.js(而非 express.js)。两者的关键区别在于:next函数返回的是一个 Promise,其值为后续中间件与 resolver 从调用栈中返回的结果。正因如此,我们可以在await next()之前和之后自由地编写逻辑,轻松实现 resolver 执行前后的动作——例如测量执行时间:
export const ResolveTime: MiddlewareFn = async ({ info }, next) => { const start = Date.now(); await next(); const resolveTime = Date.now() - start; console.log(`${info.parentType.name}.${info.fieldName} [${resolveTime} ms]`); };从类型定义(src/typings/middleware.ts)可以看到这一模型在源码中的具体形态:
export type NextFn = () => Promise<any>; export type MiddlewareFn<TContext extends object = object> = ( action: ResolverData<TContext>, next: NextFn, ) => Promise<any>; export interface MiddlewareInterface<TContext extends object = object> { use: MiddlewareFn<TContext>; }ResolverData<TContext>在 src/typings/resolver-data.ts 中定义,包含root、args、context、info四个字段,这正是中间件与 resolver 共享同一数据源的原因。
拦截执行结果:中间件的返回值语义
中间件不仅能在 resolver 执行前后“围观”,还能替换 resolver 的返回结果。这是插件系统与第三方库集成的重要基石。官方文档给出的例子是:当 resolver 返回"typegql"时,中间件将其改写为"type-graphql":
export const CompetitorInterceptor: MiddlewareFn = async (_, next) => { const result = await next(); if (result === "typegql") { return "type-graphql"; } return result; };之所以说“从库使用者的角度看似用处不大,但主要是为插件系统与第三方库集成而设计”,是因为借助这一能力,可以做到诸如:把 resolver 返回的对象包装进一个惰性关系(lazy-relation)包装器,在用户按需访问属性时才自动从数据库拉取关联数据。
这里有一个值得注意的运行时细节。在 src/resolvers/helpers.ts 的applyMiddlewares实现中,每个中间件执行完毕后有一个特殊处理:
const result = await handlerFn(resolverData, async () => { nextResult = await dispatchHandler(currentIndex + 1); return nextResult; }); return result !== undefined ? result : nextResult;也就是说:如果中间件显式返回了一个值(非undefined),该值将覆盖整个后续调用链的结果;如果中间件返回undefined,则会回退到next()链下游的返回值。这一设计让“拦截 + 改写”与“纯旁路观察”两种中间件可以统一地写在同一套模型里。测试用例 tests/functional/middlewares.ts 中的"should correctly intercept returned value"与"should correctly use next middleware value when undefined returned"两个用例分别验证了这两种行为。
简单中间件:只做执行前的事
如果只想在动作发生前做些事情(比如记录一次访问),只需要在中间件末尾放置return next():
const LogAccess: MiddlewareFn<TContext> = ({ context, info }, next) => { const username: string = context.username || "guest"; console.log(`Logging access: ${username} -> ${info.parentType.name}.${info.fieldName}`); return next(); };由于next()返回 Promise,直接return next()会让 Promise 链继续向后传递,resolver 的结果会原样返回给调用方。
守卫(Guards):中断中间件栈
中间件可以通过不调用next来主动中断中间件栈。此时,中间件自身返回的值将直接作为最终结果,resolver 根本不会被执行,也就不会有任何数据返回;也可以在需要终止执行并向用户返回错误时(例如 resolver 参数不正确),在中间件内直接throw一个错误。
由此可以构造一个阻断访问的守卫。官方文档示例中,CompetitorDetector遇到竞争对手框架名直接抛错、遇到特定写法则改写返回值,其余情况才放行:
export const CompetitorDetector: MiddlewareFn = async ({ args }, next) => { if (args.frameworkName === "type-graphql") { return "TypeGraphQL"; } if (args.frameworkName === "typegql") { throw new Error("Competitive framework detected!"); } return next(); };这一机制也正是@Authorized()授权装饰器的底层实现方式:在 src/resolvers/helpers.ts 的applyAuthChecker中,当配置了authChecker且目标带有roles时,AuthMiddleware会被unshift到中间件数组的最前面,作为守卫拦截未授权请求。其具体实现位于 src/helpers/auth-middleware.ts。
可复用中间件:中间件工厂
有些中间件需要可配置化——就像向@Authorized()装饰器传入roles数组一样。此时应创建一个中间件工厂:一个接收配置参数、返回中间件的普通函数。官方示例NumberInterceptor用于隐藏低于阈值的数字:
export function NumberInterceptor(minValue: number): MiddlewareFn { return async (_, next) => { const result = await next(); // Hide values below minValue if (typeof result === "number" && result < minValue) { return null; } return result; }; }注意:挂载时必须调用工厂函数传参,例如NumberInterceptor(3.0),而不是直接引用NumberInterceptor本身。这一示例在仓库中有完整可运行的实现:examples/middlewares-custom-decorators/middlewares/number-interceptor.ts,并在recipe.resolver.ts中通过@UseMiddleware(NumberInterceptor(3.0))使用。
错误拦截器:捕获、记录并过滤异常
中间件同样可以捕获执行过程中抛出的错误,从而完成日志记录,甚至过滤掉不能返回给用户的敏感信息(例如包含 SQL 查询语句的数据库错误):
export const ErrorInterceptor: MiddlewareFn<any> = async ({ context, info }, next) => { try { return await next(); } catch (err) { // Write error to file log fileLog.write(err, context, info); // Hide errors from db like printing sql query if (someCondition(err)) { throw new Error("Unknown error occurred!"); } // Rethrow the error throw err; } };关键点在于:try/catch包裹的是await next(),而next()返回的 Promise 会沿着调用链一直传递到 resolver 本身,因此 resolver(以及内层所有中间件)抛出的任何错误都会在这里被捕获;最后可以重新抛出(throw err)以保留原始错误信息,或抛出一个新的、经过清洗的错误。
类式中间件:结合依赖注入与可测试性
当中间件逻辑变复杂——需要访问数据库、写文件日志、需要被单元测试 mock 时,应使用类式中间件。它实现MiddlewareInterface接口,并提供一个签名与MiddlewareFn一致的use方法。这样就能受益于 dependency-injection 机制,轻松注入并 mock 一个文件记录器或数据库仓库。
下面是把前文LogAccess改造成类式中间件的官方示例:
export class LogAccess implements MiddlewareInterface<TContext> { constructor(private readonly logger: Logger) {} async use({ context, info }: ResolverData<TContext>, next: NextFn) { const username: string = context.username || "guest"; this.logger.log(`Logging access: ${username} -> ${info.parentType.name}.${info.fieldName}`); return next(); } }在源码层,Middleware<TContext>类型就是MiddlewareFn<TContext> | MiddlewareClass<TContext>的联合类型(src/typings/middleware.ts),其中MiddlewareClass是返回MiddlewareInterface实例的构造函数类型。运行时,applyMiddlewares会通过原型链判断当前中间件是函数还是类:
if (currentMiddleware.prototype !== undefined) { const middlewareClassInstance = await container.getInstance( currentMiddleware as MiddlewareClass<any>, resolverData, ); handlerFn = middlewareClassInstance.use.bind(middlewareClassInstance); } else { handlerFn = currentMiddleware as MiddlewareFn<any>; }可以看到:类式中间件的实例由 IOC 容器(container.getInstance)创建,因此构造函数中的依赖会被自动注入;use方法被bind到该实例上,再与函数式中间件走完全相同的调用链。这一实现在 src/resolvers/helpers.ts 中,容器相关逻辑位于 src/utils/container.ts。
如何挂载中间件
在 resolver 与字段上使用 @UseMiddleware()
将@UseMiddleware()装饰器放置在字段或 resolver 声明之上即可挂载中间件。它接受一个中间件数组,按传入顺序依次调用;同时也支持 rest 参数,即不必显式包裹数组:
@Resolver() export class RecipeResolver { @Query() @UseMiddleware(ResolveTime, LogAccess) randomValue(): number { return Math.random(); } }ObjectType的字段同样可以挂载中间件,用法与@Authorized()装饰器一致:
@ObjectType() export class Recipe { @Field() title: string; @Field(type => [Int]) @UseMiddleware(LogAccess) ratings: number[]; }装饰器实现(src/decorators/UseMiddleware.ts)揭示了其三种适用场景:
- 直接放在resolver 类上(
propertyKey == null分支):中间件会收集为resolverMiddlewareMetadata,对整个类的所有方法生效; - 放在方法/字段上:收集为
middlewareMetadata,绑定到对应fieldName; - 若
propertyKey是symbol,则抛出SymbolKeysNotSupportedError,即不支持 symbol 类型的属性键。
同时,@UseMiddleware通过getArrayFromOverloadedRest(src/helpers/decorators.ts)兼容“传单个数组”与“rest 参数展开”两种写法。测试用例 tests/functional/middlewares.ts 中的"should correctly call middlewares in order"验证了多个中间件按“先 before、后 after”的顺序正确执行,"should call middlewares in order of multiple decorators"则验证了叠加多个@UseMiddleware装饰器时的顺序行为。
注册全局中间件
对于耗时统计、错误捕获这类希望作用于所有 query、mutation、subscription 和字段 resolver 的通用中间件,逐个打@UseMiddleware(ResolveTime)显然繁琐。TypeGraphQL 为此提供了全局中间件:在buildSchema配置对象的globalMiddlewares属性中声明:
const schema = await buildSchema({ resolvers: [RecipeResolver], globalMiddlewares: [ErrorInterceptor, ResolveTime], });源码中BuildSchemaOptions(src/utils/buildSchema.ts)透传了SchemaGeneratorOptions的globalMiddlewares字段;构建上下文 src/schema/build-context.ts 会将其保存为静态属性(默认值为空数组[])。
在运行时,src/resolvers/create.ts 中的三种 resolver 创建函数都会执行同一拼接逻辑:
const middlewares = globalMiddlewares.concat(resolverMetadata.middlewares!);即全局中间件永远排在局部中间件之前。以普通字段为例(createBasicFieldResolver),字段级中间件同样遵循globalMiddlewares.concat(fieldMetadata.middlewares!)的顺序;随后applyAuthChecker会把授权守卫unshift到最前面,形成完整的执行链。
测试用例"should correctly call middlewares in the order of global, resolver, field"(tests/functional/middlewares.ts)给出了完整的顺序断言:globalMiddleware1 before→globalMiddleware2 before→ resolver 级中间件 → 字段级中间件 → resolver 执行 → 各级 after 逆序返回。"should correctly call global middlewares before local ones"进一步确认了全局中间件在 resolver 级局部中间件之前执行。
用自定义装饰器封装中间件
若希望中间件拥有更具描述性的声明式 API,可以基于中间件创建自定义方法装饰器,详见 custom decorators 文档 中的 “method decorators” 一节。仓库提供了开箱即用的辅助函数 src/decorators/createMethodMiddlewareDecorator.ts:
export function createMethodMiddlewareDecorator<TContextType extends object = object>( resolver: MiddlewareFn<TContextType>, ): MethodDecorator { return UseMiddleware(resolver); }它把一个中间件函数包装为标准的MethodDecorator,例如在 examples/middlewares-custom-decorators/decorators/log-message.decorator.ts 中,将日志中间件封装为@LogMessage(...)这样的语义化装饰器。此外 src/decorators/createParameterDecorator.ts 与 src/decorators/createResolverClassMiddlewareDecorator.ts 分别支持自定义参数装饰器与 resolver 类级中间件装饰器,三者在 src/decorators/index.ts 中统一导出。
底层执行原理:洋葱模型与 applyMiddlewares
将以上所有机制汇聚到一起,就是applyMiddlewares(src/resolvers/helpers.ts)实现的洋葱模型。其核心是一个递归dispatchHandler:
- 从下标 0 开始,依次取出中间件;
- 若当前下标等于中间件总数,
handlerFn就是真正的resolverHandlerFunction(即 resolver 本体); - 否则把中间件(函数式或类式)包装为
handlerFn; - 调用
handlerFn(resolverData, next),其中传给中间件的next会递归调用dispatchHandler(currentIndex + 1); - 中间件返回
undefined时使用nextResult作为透传结果,返回具体值时则覆盖结果。
另外还有一个值得注意的保护逻辑:如果某个中间件内next()被调用了多次,会抛出"next() called multiple times"错误,防止栈被重复执行。
因此,对于一个挂载了[M1, M2]的 resolver,实际调用序列为:M1 before → M2 before → resolver → M2 after → M1 after,整个过程形成一个“先进后出”的洋葱结构,与 koa 的执行模型一致。全局中间件、resolver 级中间件、字段级中间件的完整叠加顺序由create.ts中的globalMiddlewares.concat(...)保证,并已被测试用例逐条断言。
完整示例:middlewares-custom-decorators
仓库自带的 examples/middlewares-custom-decorators 示例把本文涉及的中间件类型串成了一个可运行的项目,包括:
- middlewares/log-access.ts:基于
context.username记录访问; - middlewares/resolve-time.ts:测量 resolver 执行耗时;
- middlewares/error-logger.ts:错误捕获与日志;
- middlewares/number-interceptor.ts:可配置的返回值拦截工厂;
- decorators/log-message.decorator.ts:自定义装饰器封装;
- decorators/current-user.ts:自定义参数装饰器。
该示例中的recipe.resolver.ts同时展示了@UseMiddleware(NumberInterceptor(3.0))、@UseMiddleware(LogAccess)以及自定义装饰器在真实 resolver 上的组合用法,可直接作为编写自己中间件的参考模板。
小结
- 中间件是接收
(resolverData, next)的函数,next返回 Promise,形成 koa 式洋葱模型; - 返回非
undefined值可改写 resolver 结果,不调用next或抛错可构建守卫; - 复杂逻辑应使用实现
MiddlewareInterface的类式中间件,以享受依赖注入与可测试性; - 通过
@UseMiddleware挂载局部中间件,通过buildSchema({ globalMiddlewares })注册全局中间件; - 执行顺序恒为:授权守卫(若配置)→ 全局中间件 → resolver 级中间件 → 字段级中间件 → resolver 本体,然后逆序执行 after 逻辑。
相关扩展阅读:authorization(@Authorized与守卫)、custom-decorators(自定义装饰器)、dependency-injection(类式中间件的容器注入)、middlewares(主文档)。
- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
相关推荐
TypeGraphQL 中间件与守卫(Middleware & Guards)完全指南:从装饰器到全局拦截
TypeGraphQL 中间件与守卫(Middleware & Guards)完全指南:从装饰器到全局拦截 导读 中间件(Middleware)是 TypeGr
后端GraphQLAPI设计TypeGraphQL 中间件(Middleware)与守卫(Guards)完全指南:从装饰器到全局注册的实战解析
TypeGraphQL 中间件(Middleware)与守卫(Guards)完全指南:从装饰器到全局注册的实战解析 中间件是 TypeGraphQL 中一类可复
后端GraphQLAPI设计TypeGraphQL 中间件(Middleware)与守卫(Guards)完整实战指南:从函数到类、从局部到全局
TypeGraphQL 中间件(Middleware)与守卫(Guards)完整实战指南:从函数到类、从局部到全局 本文基于 TypeGraphQL v1.0.
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考