尧图精选

TypeGraphQL 中间件与守卫(Middleware Guards)完全指南:从装饰器到洋葱模型的源码级剖析

🕒 发布时间:2026/9/28 3:43:40 📁 来源:尧图网络
后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载中间件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 () Promiseany; export type MiddlewareFnTContext extends object object ( action: ResolverDataTContext, next: NextFn, ) Promiseany; export interface MiddlewareInterfaceTContext extends object object { use: MiddlewareFnTContext; }ResolverDataTContext在 src/typings/resolver-data.ts 中定义包含root、args、context、info四个字段这正是中间件与 resolver 共享同一数据源的原因。拦截执行结果中间件的返回值语义中间件不仅能在 resolver 执行前后“围观”还能替换 resolver 的返回结果。这是插件系统与第三方库集成的重要基石。官方文档给出的例子是当 resolver 返回typegql时中间件将其改写为type-graphqlexport 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: MiddlewareFnTContext ({ 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: MiddlewareFnany 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 MiddlewareInterfaceTContext { constructor(private readonly logger: Logger) {} async use({ context, info }: ResolverDataTContext, next: NextFn) { const username: string context.username || guest; this.logger.log(Logging access: ${username} - ${info.parentType.name}.${info.fieldName}); return next(); } }在源码层MiddlewareTContext类型就是MiddlewareFnTContext | MiddlewareClassTContext的联合类型src/typings/middleware.ts其中MiddlewareClass是返回MiddlewareInterface实例的构造函数类型。运行时applyMiddlewares会通过原型链判断当前中间件是函数还是类if (currentMiddleware.prototype ! undefined) { const middlewareClassInstance await container.getInstance( currentMiddleware as MiddlewareClassany, resolverData, ); handlerFn middlewareClassInstance.use.bind(middlewareClassInstance); } else { handlerFn currentMiddleware as MiddlewareFnany; }可以看到类式中间件的实例由 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通过getArrayFromOverloadedRestsrc/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], });源码中BuildSchemaOptionssrc/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, fieldtests/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.tsexport function createMethodMiddlewareDecoratorTContextType extends object object( resolver: MiddlewareFnTContextType, ): 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将以上所有机制汇聚到一起就是applyMiddlewaressrc/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 逻辑。相关扩展阅读authorizationAuthorized与守卫、custom-decorators自定义装饰器、dependency-injection类式中间件的容器注入、middlewares主文档。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐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设计上一篇SeaTunnel Web界面怎么用5分钟打开控制台并看懂作业监控下一篇三步免费解锁Wand高级功能Wand-Enhancer上手全攻略创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →