ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

TypeGraphQL 订阅(Subscriptions)实战指南:从 pub/sub 模型到 WebSocket 服务搭建

TypeGraphQL 订阅(Subscriptions)实战指南:从 pub/sub 模型到 WebSocket 服务搭建 后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载GraphQL 除了 Query查询与 Mutation变更之外还提供第三种操作类型——Subscription订阅用于让服务器主动向客户端推送数据变更。本文以 TypeGraphQL 的Subscription()装饰器体系为核心完整讲解如何创建订阅解析器、配置 topics 与 filter、在 Mutation 中触发发布、接入 Redis 等外部 pub/sub 系统以及基于 Apollo Server 搭建支持 WebSocket 的订阅服务。读完本文你将掌握 TypeGraphQL 从订阅定义到服务端推送的完整闭环实现方案并理解其底层 schema 生成机制。为什么需要订阅从拉取到推送GraphQL 中的 Query 用于读取数据Mutation 用于写入数据但二者都属于客户端主动发起、服务端被动响应的请求-响应模型。当客户端希望在自己的数据发生变更时第一时间收到通知例如收到新评论、订单状态变化、实时消息推送就需要订阅机制——它让服务器在数据变化时主动把更新推送给已订阅的客户端。TypeGraphQL 对订阅提供了完整的支持其实现基于 Apollo 生态中的 graphql-subscriptions 包由 Apollo GraphQL 团队维护在此基础上封装出一套与 Query/Mutation 风格一致的装饰器 API让订阅解析器可以像普通类方法一样编写。订阅在底层依赖一个 pub/sub发布/订阅系统发布者publisher向某个主题topic发布消息订阅者subscriber监听主题并接收消息。创建订阅解析器订阅解析器与 Query/Mutation 解析器说明本仓库 docs 目录下的 resolvers 指南对应旧版文档可结合查看写法相似但功能上更复杂一些除了定义返回的数据形状外还需要额外声明监听哪些主题哪些事件需要触发事件如何转换为返回类型。基础定义Subscription()装饰器首先像往常一样在类中定义一个普通方法但用Subscription()装饰器标注class SampleResolver { // ... Subscription() newNotification(): Notification { // ... } }与Query()/Mutation()一致Subscription()也支持直接传入返回类型函数如Subscription(_returns Notification, {...})框架会基于方法签名与显式类型推导出订阅字段的 GraphQL 返回类型。topics订阅主题的三种声明方式使用订阅的关键是提供希望监听的主题topic。主题可以是单个字符串、字符串数组也可以是一个根据订阅参数动态生成主题的函数还可以直接使用 TypeScript 枚举增强类型安全class SampleResolver { // ... Subscription({ topics: NOTIFICATIONS, // 单个主题字符串 topics: [NOTIFICATIONS, ERRORS], // 或主题数组 topics: ({ args, payload, context }) args.topic, // 或动态主题函数 }) newNotification(): Notification { // ... } }动态主题函数的签名是({ args, payload, context }) string即SubscriptionTopicsFunc其中args是订阅操作传入的参数payload是当前事件携带的数据context是 GraphQL 上下文。这样客户端传入topic参数后订阅便只会接收对应主题的消息。在 TypeGraphQL 源码中Subscription.ts 会在收集订阅元数据时校验主题如果传入的是空数组会抛出MissingSubscriptionTopicsError防止出现无主题可订阅的静默错误。filter决定哪些事件触发订阅通过filter选项可以对主题事件做二次筛选决定哪些事件真正触发订阅。该函数必须返回boolean或Promisebooleanclass SampleResolver { // ... Subscription({ topics: NOTIFICATIONS, filter: ({ payload, args }) args.priorities.includes(payload.priority), }) newNotification(): Notification { // ... } }filter接收的上下文对象为SubscriptionHandlerData包含payload事件数据、args订阅参数、context和info。结合异步场景filter 也可以返回Promiseboolean方便进行数据库查询等异步判断。从 schema-generator.ts 的generateSubscriptionsFields实现可以看到filter 通过graphql-yoga/subscription的filter算子与pipe组合作用在 pub/sub 迭代器之上实现先过滤再向下传递的管道语义。subscribe自定义订阅逻辑某些场景下我们希望绕过默认的 pub/sub 主题机制直接提供自定义订阅逻辑例如对接 Prisma 的订阅功能。此时使用subscribe选项它应当返回一个AsyncIteratorclass SampleResolver { // ... Subscription({ subscribe: ({ args, context }) { return context.prisma.$subscribe.users({ mutation_in: [args.mutationType] }); }, }) newNotification(): Notification { // ... } }subscribe回调的入参类型为SubscribeResolverData包含source、args、context、info四要素因此可以拿到请求上下文如已注入的 Prisma client与订阅参数。注意subscribe与topics/filter不能混用。在 Subscription.ts 中SubscriptionOptions被定义为MergeExclusivePubSubOptions, SubscribeOptions即二者在类型层面就是互斥的。如果自定义订阅仍需要过滤请使用graphql-subscriptions包提供的withFilter函数手动组合。接收事件数据Root()装饰器订阅解析器方法会从 pub/sub 系统中收到事件 payload并通过Root()装饰器注入。在方法体内可以对 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(), }; } }Root()在 Root.ts 中实现它会尝试通过design:paramtypes反射推断 payload 的类型并把参数元数据标记为kind: root供 schema 生成阶段使用。它也可以像Root(someProperty)那样传入属性名只提取 payload 中的某个字段。触发订阅主题有了订阅定义之后还需要回答两个问题什么是 pub/sub 系统如何触发主题发布消息主题既可以从外部触发如数据库触发器也可以在 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; } }注入 pub/subPubSub()装饰器使用PubSub()装饰器把 pub/sub 引擎注入到方法参数中之后就可以发布主题并向所有订阅者发送 payloadclass 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; } }PubSubEngine是graphql-subscriptions定义的标准接口TypeGraphQL 侧的抽象定义见 subscriptions.tspublish(routingKey, ...args)负责发布subscribe(routingKey, dynamicId?)返回一个AsyncIterable供订阅端消费。更可测试的注入方式PubSub(TOPIC_NAME)PublisherT为了便于测试mock/stub也可以只注入绑定到指定主题的publish方法此时使用PubSub(TOPIC_NAME)与PublisherTPayload类型class SampleResolver { // ... Mutation(returns Boolean) async addNewComment( Arg(comment) input: CommentInput, PubSub(NOTIFICATIONS) publish: PublisherNotificationPayload, ) { const comment this.commentsService.createNew(input); await this.commentsRepository.save(comment); // 直接向 NOTIFICATIONS 主题发布 await publish({ message: input.content }); return true; } }这种方式下publish是一个已经绑定好主题的发布函数签名更窄、更易 mock从源码看发布动作会调用底层PubSub.publish(routingKey, payload)且PublisherTPayload是 TypeGraphQL 提供的泛型发布器类型。仓库示例 simple-subscriptions/notification.resolver.ts 中即同时演示了注入完整pubSub与直接publish的两种写法。至此所有订阅NOTIFICATIONS主题的 subscription 都会在addNewCommentMutation 执行时被触发。使用自定义 PubSub 系统如 Redis默认情况下TypeGraphQL 使用graphql-subscriptions自带的、基于 EventEmitter 的简单 PubSub 实现。它的缺陷很直接只对单实例单进程的 Node.js 应用有效多实例部署时各进程之间无法共享订阅状态。为了水平扩展应该使用基于外部存储如 Redis的 pub/sub 实现例如graphql-redis-subscriptions等。接入方式非常简单按对应包的说明创建好 PubSub 实例然后在buildSchema选项中传入const myRedisPubSub getConfiguredRedisPubSub(); const schema await buildSchema({ resolvers: [__dirname /**/*.resolver.ts], pubSub: myRedisPubSub, });pubSub选项在 build-context.ts 的BuildContextOptions中定义会被写入静态的BuildContext.pubSub供 schema 生成阶段读取。与之对应如果定义了订阅却忘了在buildSchema中提供pubSubschema 生成时会抛出 MissingPubSubError提示信息明确Looks like youve forgot to providepubSuboption inbuildSchema()——这也是源码对订阅离不开 pub/sub 系统这一前提的直接印证。仓库提供了一个基于 Redis 的完整生产示例 redis-subscriptions其 pubsub.ts 使用graphql-yoga/subscription的createPubSub配合createRedisEventTarget与 ioredis 客户端构建跨进程共享的 pub/sub并通过REDIS_URL环境变量配置连接地址。运行该示例需要本地有一个可用的 Redis 实例并可能需要按实际环境调整连接参数。创建订阅服务器WebSocket 传输层引导指南与之前章节中的示例都使用apollo-server来为 GraphQL API 提供 HTTP 端点。好消息是订阅不需要你手动实现传输层。HTTP 是请求-响应式的而订阅需要服务端主动推送因此底层依赖 WebSocket 这类双向、持久连接。apollo-server内置了基于 WebSocket 的订阅支持开箱即用无需改动 bootstrap 配置。如果希望自定义可以在ApolloServer构造配置中提供subscriptions属性// Create GraphQL server const server new ApolloServer({ schema, subscriptions: { path: /subscriptions, // 其他选项与钩子例如 onConnect }, });配置完成后服务就在/subscriptions路径上同时提供 WebSocket 订阅端点与普通的 HTTP GraphQL 端点并行工作客户端可通过subscriptions-transport-ws或新版协议建立 WebSocket 连接并执行订阅操作。结合示例理解完整链路仓库中的 simple-subscriptions 示例 覆盖了本文讨论的全部核心场景非常值得对照阅读普通订阅normalSubscription用Subscription({ topics: Topic.NOTIFICATIONS })订阅单个枚举主题方法内通过Root()解构 payload带过滤的订阅subscriptionWithFilter使用filter: ({ payload }) payload.id % 2 0只推送 id 为偶数的通知演示了filter的布尔判断语义多主题订阅subscriptionWithMultipleTopics用主题数组[Topic.NOTIFICATIONS, NOTIFICATIONS_2]同时监听两个主题并叠加 filter动态主题subscribeToTopicFromArg通过topics: ({ args }) args.topic让客户端用参数指定要订阅的主题动态 topicIdsubscribeToTopicIdFromArg与topicId: ({ args }) args.topicId演示了在固定主题下按动态 id 做进一步路由的能力触发发布pubSubMutation与publishToDynamicTopic两个 Mutation 分别演示了向固定主题和动态主题发布 payload。而在底层schema-generator.ts 的buildRootSubscriptionType会收集所有订阅处理器生成名为Subscription的根类型generateSubscriptionsFields则负责把每个订阅字段组装为标准的 GraphQL 字段定义根据是否提供subscribe决定走自定义订阅还是pub/sub topics topicId filter管道没有配置pubSub时抛错最终用wrapResolverWithAuthChecker包裹subscribeFn使订阅字段与 Query/Mutation 一样可以挂载鉴权中间件。这意味着订阅同样能被Authorized()与中间件体系保护做到了与普通解析器一致的表达能力。小结TypeGraphQL 的订阅能力把pub/sub 发布订阅模型封装成了与 Query/Mutation 一致的声明式 API用Subscription()声明订阅、用topics/filter/subscribe控制事件来源与筛选、用Root()接收 payload、用PubSub()注入发布能力、通过buildSchema的pubSub选项替换为 Redis 等可扩展实现最后交给 Apollo Server 的 WebSocket 端点对外服务。从Subscription()装饰器的元数据收集Subscription.ts到 schema 生成期的字段组装schema-generator.ts再到两个开箱即用的示例simple-subscriptions、redis-subscriptions整条链路都有源码与实例可供验证读者可以据此快速在自己的 TypeGraphQL 应用中落地实时推送能力。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐TypeGraphQL 订阅Subscriptions实战指南基于 Pub/Sub 与 Subscription 构建实时 GraphQL APITypeGraphQL 订阅Subscriptions实战指南基于 Pub/Sub 与 Subscription 构建实时 GraphQL API 导读后端GraphQLAPI设计使用 TypeGraphQL 构建 GraphQL 订阅Subscriptions从 Pub/Sub 事件到实时推送的完整实战使用 TypeGraphQL 构建 GraphQL 订阅Subscriptions从 Pub/Sub 事件到实时推送的完整实战 GraphQL 的第三种操后端GraphQLAPI设计TypeGraphQL 订阅Subscriptions完整实战指南从装饰器到自定义 PubSub 与 WebSocket 服务端TypeGraphQL 订阅Subscriptions完整实战指南从装饰器到自定义 PubSub 与 WebSocket 服务端 TypeGraphQL后端GraphQLAPI设计创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表