ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

TypeGraphQL 订阅(Subscriptions)实战指南:从 topics 到自定义 PubSub 的完整实现

2026/9/28 2:23:41 拓冰建站 浏览量
TypeGraphQL 订阅(Subscriptions)实战指南:从 topics 到自定义 PubSub 的完整实现 后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载GraphQL 的第三种操作类型 Subscription订阅让服务端能够在数据变化时主动向客户端推送更新是构建实时通知、动态列表刷新等场景的核心能力。TypeGraphQL 对订阅提供了开箱即用的支持底层基于graphql-yoga/subscriptions包实现。本文以 TypeGraphQL v2.0.0-rc.1 的官方文档为主线结合仓库源码与内置示例带你完整掌握订阅的创建、触发、动态 topic、自定义 PubSub 系统以及订阅服务器的搭建方案。为什么需要订阅在 GraphQL 中Query 用于读取数据Mutation 用于写入数据而 Subscription 则是第三种操作类型客户端订阅某个主题topic当服务端数据发生变化时服务端主动向所有订阅者推送事件。这解决了客户端需要轮询才能感知数据变化的问题是构建聊天室、实时评论、告警通知等功能的基石。TypeGraphQL 对订阅的原生支持基于 The Guild 团队开发的graphql-yoga/subscriptions直接导入了该包中的Repeater、filter、pipe等工具来构建订阅字段的底层执行逻辑。创建订阅Subscription()装饰器订阅解析器与 Query/Mutation 解析器 类似但稍复杂一些。第一步是像往常一样定义普通类方法只不过用Subscription()装饰器标注class SampleResolver { // ... Subscription() newNotification(): Notification { // ... } }从 Subscription.ts 源码 可以看到Subscription()的核心配置项被分为两组互斥选项通过MergeExclusive类型约束PubSubOptionstopics、topicId、filter基于 pubsub 系统的标准订阅流程SubscribeOptionssubscribe完全自定义订阅逻辑。两者不能混用。此外装饰器还支持传入返回类型函数作为第一参数如Subscription(_returns Notification, { topics: ... })这在示例代码中非常常见。指定 topics订阅哪个主题订阅必须提供希望订阅的 topics可以是单个字符串、字符串数组也可以是根据订阅参数动态生成 topic 的函数还支持使用 TypeScript 枚举增强类型安全class SampleResolver { // ... Subscription({ topics: NOTIFICATIONS, // 单个 topic topics: [NOTIFICATIONS, ERRORS], // topics 数组 topics: ({ args, context }) args.topic, // 动态生成 topic 的函数 }) newNotification(): Notification { // ... } }从 types.ts 的类型定义 可知动态 topic 函数接收SubscribeResolverData包含source、args、context、info四个字段见 SubscribeResolverData.ts返回string | string[]。注意一个边界情况当topics配置为空数组[]时Subscription.ts 源码 会直接抛出MissingSubscriptionTopicsError因此 topics 不能为空数组。用 filter 过滤事件filter选项用于决定哪些 topic 事件才会真正触发订阅。该函数应返回boolean或Promisebooleanclass SampleResolver { // ... Subscription({ topics: NOTIFICATIONS, filter: ({ payload, args }) args.priorities.includes(payload.priority), }) newNotification(): Notification { // ... } }filter 的类型定义 表明它接收SubscriptionHandlerData——一个包含payload、args、context、info的对象见 SubscriptionHandlerData.ts。其中payload是发布方发送的数据args是客户端订阅时的参数因此可以实现只接收符合参数条件的事件这类精细化过滤。自定义 subscribe 逻辑如果内置的 topics filter 机制不满足需求例如要对接 Prisma 等 ORM 自带的订阅功能可以使用subscribe选项它应是一个返回AsyncIterable或PromiseAsyncIterable的函数。下面的例子来自文档展示了如何使用 Prisma 1 的订阅能力class SampleResolver { // ... Subscription({ subscribe: ({ root, args, context, info }) { return context.prisma.$subscribe.users({ mutation_in: [args.mutationType] }); }, }) newNotification(): Notification { // ... } }subscribe函数同样接收SubscribeResolverData注意其参数名为source而非root见类型定义。这里传入的root是文档示例中的写法实际类型字段名为source。注意subscribe选项与topics、filter选项不能混用。如果自定义订阅后仍需要过滤可以借助graphql-yoga/subscriptions包提供的filter与map辅助函数在事件流上做处理。用 Root() 接收 payload 并转换订阅解析器方法本身会收到由 pubsub 系统触发的 topic 事件负载payload通过Root()装饰器注入然后可以在方法体内将其转换为最终返回给客户端的形状class SampleResolver { // ... Subscription({ topics: NOTIFICATIONS, filter: ({ payload, args }) args.priorities.includes(payload.priority), }) newNotification( Root() notificationPayload: NotificationPayload, Args() args: NewNotificationsArgs, ): Notification { return { ...notificationPayload, date: new Date(), }; } }触发订阅 topics创建好订阅后下一个问题是如何触发。触发可以来自外部数据源如数据库变更也可以来自 Mutation——当某个资源被修改、而客户端希望收到变更通知时。假设我们有一个新增评论的 Mutationclass SampleResolver { // ... Mutation(returns Boolean) async addNewComment(Arg(comment) input: CommentInput) { const comment this.commentsService.createNew(input); await this.commentsRepository.save(comment); return true; } }第一步创建 PubSub 实例大多数情况下直接调用graphql-yoga/subscriptions包导出的createPubSub()函数即可。它支持通过类型参数定义每个 topic 对应的 payload 元组从而获得编译期类型安全import { createPubSub } from graphql-yoga/subscriptions; export const pubSub createPubSub{ NOTIFICATIONS: [NotificationPayload]; DYNAMIC_ID_TOPIC: [number, NotificationPayload]; }();第二步在 buildSchema() 中注册将 PubSub 实例传给buildSchema()的pubSub选项import { buildSchema } from type-graphql; import { pubSub } from ./pubsub; const schema await buildSchema({ resolver, pubSub, });第三步在 Mutation 中发布事件最后在 Mutation 解析器中注入 PubSub 并调用publish()触发 topic向所有订阅者发送 payloadimport { pubSub } from ./pubsub; class SampleResolver { // ... Mutation(returns Boolean) async addNewComment(Arg(comment) input: CommentInput, PubSub() pubSub: PubSubEngine) { const comment this.commentsService.createNew(input); await this.commentsRepository.save(comment); // Trigger subscriptions topics const payload: NotificationPayload { message: input.content }; pubSub.publish(NOTIFICATIONS, payload); return true; } }完成这三步后所有订阅了NOTIFICATIONStopic 的订阅都会在执行addNewCommentMutation 时被触发。仓库示例中的完整闭环在 simple-subscriptions 示例 中可以看到一条完整链路pubsub.ts 通过createPubSub创建实例并用枚举Topic定义常量notification.resolver.ts 的pubSubMutation发布事件normalSubscription订阅后经Root()取出NotificationPayload组装出带date的Notification返回。subscriptionWithFilter则演示了filter的用法payload.id % 2 0只接受偶数 id 的事件。带动态 ID 的 topicDynamic Topic ID该特性灵感同样来自底层使用的graphql-yoga/subscriptions。有些场景下你只希望为某个特定实体如某个用户或商品收发事件——此时可以用动态 topic ID 把 topic 限定到特定标识符上Resolver() class NotificationResolver { Subscription({ topics: NOTIFICATIONS, topicId: ({ context }) context.userId, }) newNotification(Root() { message }: NotificationPayload): Notification { return { message, date: new Date() }; } }发布时需要把 topic id 作为publish()的第二个参数传入pubSub.publish(NOTIFICATIONS, userId, { id, message });仓库的 simple-subscriptions 示例 中topicId函数从订阅参数args.topicId中取值而publishWithDynamicTopicIdMutation 则以pubSub.publish(Topic.DYNAMIC_ID_TOPIC, topicId, payload)形式发布两处标识保持一致。注意动态 topic ID 需要 pubsub 系统本身的支持。如果使用的不是graphql-yoga/subscriptions的createPubSub()publish()的第二个参数可能被当作 payload 而不是动态 topic id需要提前确认所选实现的语义。使用自定义 PubSub 系统TypeGraphQL 虽然在内部使用graphql-yoga/subscriptions处理订阅但并不强制要求使用它的 PubSub 实现。任何满足导出的PubSub接口的 pubsub 系统都可以接入只要具备正确的.subscribe()与.publish()方法。从 src/typings/subscriptions.ts 的 PubSub 接口 可以看到TypeGraphQL 只要求实现者提供两个方法publish(routingKey: string, ...args: unknown[]): void——发布事件支持可变参数这也是动态 topic ID 能作为第二参数传入的原因subscribe(routingKey: string, dynamicId?: unknown): AsyncIterableunknown——返回一个异步可迭代对象作为事件流。这一点在生产环境尤其重要内存事件发射器in-memory event emitter无法跨进程工作多实例部署时需要改用分布式 pubsub例如基于 Redis 的实现确保所有实例共享同一事件通道。生产级示例Redis 分布式 PubSub仓库提供了基于 Redis 的完整示例。其 pubsub.ts 通过graphql-yoga/redis-event-target的createRedisEventTarget创建事件目标配置一对 ioredis 客户端一个用于 publish、一个用于 subscribe并设置了retryStrategy重连策略export const pubSub createPubSub{ [Topic.NEW_COMMENT]: [NewCommentPayload]; }({ eventTarget: createRedisEventTarget({ publishClient: new Redis(redisUrl, { retryStrategy: times Math.max(times * 100, 3000), }), subscribeClient: new Redis(redisUrl, { retryStrategy: times Math.max(times * 100, 3000), }), }), });示例的REDIS_URL通过环境变量读取运行前需要启动 Redis 实例并可能要根据实际情况修改连接参数。recipe.resolver.ts 展示了经典的评论订阅业务addNewCommentMutation 在保存评论后publish到NEW_COMMENTtopicnewComments订阅用filter按recipeId过滤确保客户端只收到自己关注菜谱的新评论。由于 Redis 跨进程传输会对 payload 做序列化示例在订阅端用new Date(newComment.dateString)恢复Date对象见代码注释中的说明。创建订阅服务器Subscription Server文档此前所有示例包括 bootstrap 指南都使用 apollo-server 创建 GraphQL HTTP 端点。但从 Apollo Server 3 开始内置的 batteries-includedapollo-server包不再支持订阅需要按官方文档指引手动开启订阅能力。如果不想处理 Apollo Server 的订阅配置可以直接使用graphql-yoga——仓库内置示例 simple-subscriptions/index.ts 展示了极简的订阅服务器搭建方式buildSchema()生成可执行 schema 并传入pubSub随后createYoga({ schema, graphqlEndpoint: /graphql })创建 GraphQL Yoga 服务最后挂载到node:http服务器并监听 4000 端口即可。整个过程无需额外配置订阅功能开箱即用。订阅开发速查表配置项类型作用约束topicsstring \| string[] \| ({ args, context }) string \| string[]指定订阅的主题不能与subscribe混用不能为空数组topicId({ args, context }) any声明动态 ID topic 的标识来源需 pubsub 系统支持filter({ payload, args, context, info }) boolean \| Promiseboolean按事件内容与订阅参数过滤不能与subscribe混用subscribe({ source, args, context, info }) AsyncIterable \| PromiseAsyncIterable完全自定义订阅逻辑不能与topics、filter混用buildSchema的pubSub实现PubSub接口的实例全局注册 pubsub 系统必须有.publish()与.subscribe()PubSub()参数装饰器在解析器方法中注入 PubSub 实例用于在 Mutation 中发布事件Root()参数装饰器注入事件 payload用于订阅解析器方法小结TypeGraphQL 的订阅能力覆盖了从定义订阅topics / filter / 自定义 subscribe、在 Mutation 中触发事件到动态 topic ID 限定实体维度、接入自定义 PubSub含 Redis 分布式方案的完整链路。核心要点可归纳为Subscription()配置中topicsfilter与subscribe是互斥的两条路径事件触发统一走pubSub.publish(topic, payload)而publish()的可变参数设计天然支持动态 topic ID接入自定义 PubSub 只需实现publish与subscribe两个方法生产环境推荐基于 Redis 的分布式实现服务器层面若使用 Apollo Server 3 需手动开启订阅支持或直接选用 graphql-yoga 获得开箱即用的订阅体验。想动手验证可以直接参考仓库中的 simple-subscriptions内存版与 redis-subscriptionsRedis 分布式版两个示例后者运行前需准备好 Redis 实例。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐TypeGraphQL 订阅Subscriptions完整实战指南从装饰器到自定义 PubSub 与 WebSocket 服务端TypeGraphQL 订阅Subscriptions完整实战指南从装饰器到自定义 PubSub 与 WebSocket 服务端 TypeGraphQL后端GraphQLAPI设计TypeGraphQL 订阅Subscriptions完整实战指南Subscription 装饰器、PubSub 主题与分布式部署TypeGraphQL 订阅Subscriptions完整实战指南Subscription 装饰器、PubSub 主题与分布式部署 GraphQL 提供后端GraphQLAPI设计TypeGraphQL 订阅Subscriptions实战指南从 topic 发布订阅到 Redis 扩展TypeGraphQL 订阅Subscriptions实战指南从 topic 发布订阅到 Redis 扩展 GraphQL 除了查询Query与变更后端GraphQLAPI设计上一篇FastF1 高精度遥测计算指南验证、插值与按圈切片的最佳实践下一篇Anki-Connect 高级技巧实现跨平台闪卡同步与自动化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考