☰
TypeGraphQL 订阅(Subscriptions)完整实战指南:@Subscription 装饰器、PubSub 主题与分布式部署
2026/9/27 7:00:20 网站建设 项目流程
  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载

GraphQL 提供 Query 与 Mutation 分别满足读取和写入场景,但客户端往往还希望在关注的数据发生变化时,被服务器主动推送更新。为此 GraphQL 定义了第三种操作——Subscription(订阅)。本指南以 TypeGraphQL 的官方订阅文档为主体,结合仓库源码与 simple-subscriptions 示例,系统讲解如何使用@Subscription()装饰器声明订阅解析器、通过pubsub系统发布主题事件、按需过滤与动态主题、接入 Redis 等分布式 PubSub 以及搭建支持 WebSocket 的订阅服务端。读完本文,你将能独立实现一个可扩展、可测试的 GraphQL 实时推送 API。

一、订阅在 GraphQL 中的定位

GraphQL 用 Query 执行读取、用 Mutation 执行写入,而Subscription 用于服务器向客户端持续推送数据变更。TypeGraphQL 原生支持订阅,并借助graphql-subscriptions(Apollo GraphQL 团队维护)这一类 PubSub 实现完成事件的发布与订阅。在 0.16.0 版本中,@Subscription()装饰器、@PubSub()参数装饰器与buildSchema的pubSub选项构成了完整的订阅链路:

  • src/decorators/Subscription.ts负责收集订阅元数据;
  • src/schema/schema-generator.ts负责把订阅元数据转换成 GraphQL 的subscribe字段;
  • 运行期需要一个 PubSub 实例(默认基于EventEmitter的进程内实现)。

订阅解析器的整体形态与 Query/Mutation 解析器 类似,但多了一层"监听主题 → 接收载荷 → 转换返回值"的处理,因此稍显复杂。

二、创建第一个订阅解析器

2.1 最小订阅:@Subscription 装饰器

订阅解析器同样是一个普通类方法,只是改用@Subscription()装饰器标记:

class SampleResolver { // ... @Subscription() newNotification(): Notification { // ... } }

注意:仅有@Subscription()而没有提供主题时,订阅只是声明了返回类型;真正可运行还需要配合下面的topics(或subscribe)选项。

2.2 指定订阅主题:topics

需要告诉 TypeGraphQL 我们要订阅哪些主题(topics)。topics支持三种形态,同时推荐用 TypeScript 枚举提升类型安全(参见examples/simple-subscriptions/pubsub.ts中export const enum Topic的用法):

class SampleResolver { // ... @Subscription({ topics: "NOTIFICATIONS", // 单个主题 topics: ["NOTIFICATIONS", "ERRORS"] // 或主题数组 topics: ({ args, payload, context }) => args.topic // 或动态主题函数 }) newNotification(): Notification { // ... } }
  • 单个主题字符串:如"NOTIFICATIONS",订阅所有发布到该主题的事件;
  • 主题数组:同时监听多个主题。从源码看,多个主题会通过Repeater.merge([...topics.map(topic => pubSub.subscribe(topic, topicId))])合并成同一个异步迭代器(src/schema/schema-generator.ts),任一主题有事件都会触发解析器;
  • 动态主题函数:接收{ args, payload, context }(即SubscribeResolverData,包含source/args/context/info),依据订阅查询参数动态决定主题,典型场景如"客户端传入想订阅的话题名"。

空数组校验:@Subscription({ topics: [] })会在装饰器求值阶段直接抛出MissingSubscriptionTopicsError(src/decorators/Subscription.ts);运行时若解析后主题仍为空数组,src/schema/schema-generator.ts同样会抛出该错误,保证不会出现静默的无效订阅。

2.3 过滤事件:filter

并非主题上的每个事件都应该推送给订阅者。filter选项用于决定哪些事件触发订阅,函数签名接收{ payload, args, context, info },必须返回boolean或Promise<boolean>:

class SampleResolver { // ... @Subscription({ topics: "NOTIFICATIONS", filter: ({ payload, args }) => args.priorities.includes(payload.priority), }) newNotification(): Notification { // ... } }

上述示例只有当载荷的priority出现在订阅参数priorities中时,事件才会真正派发给该订阅者。底层实现中,TypeGraphQL 用pipe(pubSubIterable, filter(payload => ...))将过滤逻辑串入异步迭代流(src/schema/schema-generator.ts),因此过滤发生在订阅者的数据流内部,而非发布端。

2.4 接收载荷并转换返回值:@Root

主题被触发后,订阅解析器通过@Root()装饰器接收来自 pubsub 的载荷(payload),并在方法体内把它转换为订阅字段要求的返回形状:

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(), }; } }

在examples/simple-subscriptions/notification.resolver.ts中可以看到真实用法:normalSubscription直接把NotificationPayload展开并补上date字段;subscriptionWithFilter则演示了filter: ({ payload }) => payload.id % 2 === 0的奇偶过滤。

三、发布主题事件:触发订阅的 pubsub

3.1 pubsub 是什么

上文一直在说"触发主题",触发动作来自pubsub(发布/订阅)系统。事件可能来自数据库的外部变更,也可以在我们自己的 Mutation 中触发——比如修改了某个客户端关心的资源时发布通知。

假设我们有这样一个"添加评论"的 Mutation:

class SampleResolver { // ... @Mutation(returns => Boolean) async addNewComment(@Arg("comment") input: CommentInput) { const comment = this.commentsService.createNew(input); await this.commentsRepository.save(comment); return true; } }

3.2 注入 PubSub 实例:@PubSub()

使用@PubSub()参数装饰器把pubsub注入到方法参数中,即可在任意地方publish主题并广播载荷给所有订阅者:

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); // 触发订阅主题 const payload: NotificationPayload = { message: input.content }; await pubSub.publish("NOTIFICATIONS", payload); return true; } }

调用pubSub.publish("NOTIFICATIONS", payload)后,所有绑定到NOTIFICATIONS主题的订阅都会被触发。

3.3 只注入 publish:@PubSub("TOPIC_NAME")

为了便于测试(更容易 mock/stub),也可以只注入绑定到指定主题的publish方法,配合Publisher<TPayload>类型:

class SampleResolver { // ... @Mutation(returns => Boolean) async addNewComment( @Arg("comment") input: CommentInput, @PubSub("NOTIFICATIONS") publish: Publisher<NotificationPayload>, ) { const comment = this.commentsService.createNew(input); await this.commentsRepository.save(comment); // 触发订阅主题 await publish({ message: input.content }); return true; } }

相比直接注入整个 PubSub 实例,这种写法把"发布到哪个主题"的职责收拢到参数级别,单测中只需 stub 一个publish函数即可,无需构造完整 PubSub 引擎。

至此,执行addNewCommentMutation 时,所有订阅了NOTIFICATIONS主题的 Subscription 都会被触发。

3.4 发布端的实现细节

在 0.16.0 时代的实现中,publish是 PubSub 引擎的标准能力。仓库示例examples/simple-subscriptions/notification.resolver.ts展示了在 Mutation 中直接pubSub.publish(Topic.NOTIFICATIONS, payload)的完整链路,而examples/simple-subscriptions/pubsub.ts中通过createPubSub<{ [Topic.NOTIFICATIONS]: [NotificationPayload] }>()预先声明了主题与载荷类型的映射,让publish与subscribe在编译期就能得到类型检查。

四、自定义 PubSub 系统:从进程内到分布式

4.1 默认实现的局限

默认情况下,TypeGraphQL 使用graphql-subscriptions中基于EventEmitter的简单PubSub。这种方案有一个显著缺陷:只在 Node.js 应用为单实例(单进程)时才能正确工作——事件发生在进程 A,进程 B 的订阅者完全收不到。

4.2 接入外部存储的 PubSub 实现

为了更好的可扩展性,应选用由外部存储支撑的 PubSub 实现,例如基于 Redis 的graphql-redis-subscriptions包。接入方式非常简单:按包说明创建 PubSub 实例,然后在buildSchema选项中传入即可:

const myRedisPubSub = getConfiguredRedisPubSub(); const schema = await buildSchema({ resolvers: [__dirname + "/**/*.resolver.ts"], pubSub: myRedisPubSub, });

buildSchema的pubSub选项是订阅功能的开关:从源码看,当存在订阅处理器而pubSub未提供时,schema 生成阶段会抛出MissingPubSubError(src/schema/schema-generator.ts),防止生成一个无法工作的订阅 Schema。

4.3 Redis 订阅的生产级示例

仓库中的 redis-subscriptions 示例 演示了生产环境推荐方案。其pubsub.ts使用@graphql-yoga/redis-event-target的createRedisEventTarget创建发布/订阅双客户端,并配置了retryStrategy重连策略:

import { createRedisEventTarget } from "@graphql-yoga/redis-event-target"; import { createPubSub } from "@graphql-yoga/subscription"; import { Redis } from "ioredis"; const redisUrl = process.env.REDIS_URL; if (!redisUrl) { throw new Error("REDIS_URL env variable is not defined"); } 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 客户端在订阅模式下无法执行普通命令的问题;
  • 重试策略:retryStrategy让连接中断后按指数退避自动重连;
  • 运行前提:需要本地有运行中的 Redis 实例,且可能需按实际连接参数修改示例代码。

五、创建订阅服务端:WebSocket 传输

bootstrap 指南 与之前的示例都用apollo-server创建 GraphQL API 的 HTTP 端点。好消息是:订阅不需要我们手动实现传输层。HTTP 无法做到真正的推送式通信,订阅依赖 WebSocket;而apollo-server内置了基于 WebSocket 的订阅支持,开箱即用,无需改动 bootstrap 配置。

如果愿意,也可以显式提供subscriptions配置项来定制路径与钩子:

// 创建 GraphQL server const server = new ApolloServer({ schema, subscriptions: { path: "/subscriptions", // 其他选项与钩子,如 onConnect }, });

完成之后,我们就在/subscriptions路径上得到了一个可用的 GraphQL 订阅服务端,同时保留原来的 HTTP GraphQL 服务端。

六、从 0.16.0 到现代版本的订阅演进

本文对应的文档版本(0.16.0)以graphql-subscriptions+PubSubEngine/Publisher类型为基础设施。仓库最新文档 docs/subscriptions.md 则展示了订阅 API 的演进:

  • PubSub 创建方式:现代版本推荐createPubSub()(来自@graphql-yoga/subscription),并支持用泛型声明主题与载荷类型映射(createPubSub<{ NOTIFICATIONS: [NotificationPayload] }>());
  • 自定义 subscribe 逻辑:新增subscribe选项,可直接返回AsyncIterable或Promise<AsyncIterable>(例如对接 Prisma 订阅能力),但不能与topics/filter混用;
  • 动态主题 ID(topicId):支持将主题限定到具体实体 ID(如topicId: ({ context }) => context.userId),发布时以第二个参数传入 ID,实现按实体隔离的事件流;
  • PubSub 接口约定:任何 PubSub 系统只要满足publish(routingKey, ...args)与subscribe(routingKey, dynamicId?)两个方法即可接入(见src/typings/subscriptions.ts导出的PubSub接口);
  • Apollo Server 3 之后:apollo-server不再内置订阅支持,需要按官方文档手动开启(更现代的方案是使用graphql-yoga,见 simple-subscriptions 示例 中createYoga的用法)。

七、端到端示例:simple-subscriptions 全流程

仓库的 simple-subscriptions 示例 是理解订阅全流程的最佳教材:

  1. 定义类型与载荷:notification.type.ts声明Notification(对外返回)与NotificationPayload(发布载荷);
  2. 创建 PubSub 并声明主题:pubsub.ts中export const enum Topic定义主题常量,createPubSub<...>()建立主题与载荷的类型映射;
  3. 编写订阅解析器:notification.resolver.ts演示了无过滤订阅、filter过滤订阅、多主题订阅(topics: [Topic.NOTIFICATIONS, "NOTIFICATIONS_2"])、基于参数动态主题(topics: ({ args }) => args.topic)以及动态主题 ID(topicId)五种订阅形态,以及配套的pubSubMutation、publishToDynamicTopic、publishWithDynamicTopicId三个发布 Mutation;
  4. 启动服务:index.ts中buildSchema({ resolvers: [NotificationResolver], pubSub })构建 Schema,再用graphql-yoga的createYoga({ schema })创建同时支持 HTTP 与 WebSocket 的服务器。

八、最佳实践小结

  • 用枚举管理主题:把主题字符串收敛为const enum,避免拼写错误,同时获得类型提示;
  • 过滤放在订阅端:filter在订阅者的异步迭代器内部执行,可按不同订阅参数各自过滤,发布端只需广播全量事件;
  • 发布端注入最小依赖:用@PubSub("TOPIC")注入绑定主题的publish,便于单测 mock;
  • 生产环境务必使用分布式 PubSub:单进程 EventEmitter 在部署多实例时会丢失跨进程事件,Redis 等外部存储是推荐方案;
  • 确保buildSchema传入pubSub:否则存在订阅时 schema 生成会抛出MissingPubSubError;
  • 注意版本差异:0.16.0 基于graphql-subscriptions,现代版本基于@graphql-yoga/subscription的createPubSub与PubSub接口,迁移时需同步调整发布端代码与服务器配置(Apollo Server 3+ 需手动启用订阅或改用 graphql-yoga)。
  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询