ARTICLE DETAIL

资讯详情

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

RS School App 的 NestJS 事件驱动架构实践:基于 @nestjs/event-emitter 实现模块解耦

RS School App 的 NestJS 事件驱动架构实践:基于 @nestjs/event-emitter 实现模块解耦 RS School App 的 NestJS 事件驱动架构实践基于 nestjs/event-emitter 实现模块解耦【免费下载链接】rsschool-appAn application for the RS School education process项目地址: https://gitcode.com/gh_mirrors/rs/rsschool-app导读本文围绕.agents/skills/nestjs-best-practices/rules/arch-use-events.md中定义的架构规则展开讲解如何在 RS School App 的 NestJS 后端nestjs/目录中使用nestjs/event-emitter实现服务内事件驱动解耦并通过消息代理实现服务间通信。读完本文你将掌握事件驱动架构的核心价值、事件定义与监听的最佳写法以及当前仓库对事件基础设施的落地方式能够在实际模块开发中避免直接服务耦合、优雅地横向扩展业务行为。一、规则背景为什么要用事件驱动解耦在单体化的 NestJS 应用中最常见的耦合反模式是一个核心服务如OrdersService在完成自身业务后还要显式调用多个下游服务的同步方法——扣库存、发邮件、埋点统计、推送通知、积分累加。随着业务增长新增一种下单后行为就必须修改OrdersService的构造器与createOrder方法服务之间的依赖关系越来越难以维护。arch-use-events.md规则给出明确结论服务内使用nestjs/event-emitter服务间使用消息代理message brokers。事件允许模块对变更做出反应而无需直接依赖从而提升模块化程度并支持异步处理。该规则的影响级别为 MEDIUM-HIGH因为它直接决定了模块间的耦合程度与系统的可扩展能力。反模式示例直接服务耦合原规则文档给出了典型反例——OrdersService的构造函数注入了InventoryService、EmailService、AnalyticsService、NotificationService、LoyaltyService五个服务Injectable() export class OrdersService { constructor( private inventoryService: InventoryService, private emailService: EmailService, private analyticsService: AnalyticsService, private notificationService: NotificationService, private loyaltyService: LoyaltyService, ) {} async createOrder(dto: CreateOrderDto): PromiseOrder { const order await this.repo.save(dto); // Tight coupling - OrdersService knows about all consumers await this.inventoryService.reserve(order.items); await this.emailService.sendConfirmation(order); await this.analyticsService.track(order_created, order); await this.notificationService.push(order.userId, Order placed); await this.loyaltyService.addPoints(order.userId, order.total); // Adding new behavior requires modifying this service return order; } }这段代码的问题非常直观发布者知道所有消费者OrdersService必须了解每个下游服务的接口签名任何一处变更都会波及它新增行为成本高每增加一个下单后动作都要改动OrdersService的构造器和方法体无法异步化所有下游调用都是同步 await下单链路被最慢的消费者拖累测试困难需要 mock 五个依赖才能单测createOrder。二、正确姿势事件驱动的完整实现1. 定义事件类事件类通常是一个纯数据类POJO用readonly字段保证不可变性命名上使用过去分词如OrderCreatedEvent与已经发生的事实语义一致export class OrderCreatedEvent { constructor( public readonly orderId: string, public readonly userId: string, public readonly items: OrderItem[], public readonly total: number, ) {} }事件类可以放在独立的events/目录下便于被生产方与消费方共同引用避免出现循环依赖可参考仓库中 arch-avoid-circular-deps.md 规则。2. 服务只负责发布事件改造后的OrdersService只注入EventEmitter2和数据仓库调用eventEmitter.emit(order.created, event)后立即返回对消费者一无所知import { EventEmitter2 } from nestjs/event-emitter; Injectable() export class OrdersService { constructor( private eventEmitter: EventEmitter2, private repo: RepositoryOrder, ) {} async createOrder(dto: CreateOrderDto): PromiseOrder { const order await this.repo.save(dto); // Emit event - no knowledge of consumers this.eventEmitter.emit(order.created, new OrderCreatedEvent(order.id, order.userId, order.items, order.total)); return order; } }关键变化是发布者与消费者彻底解耦。OrdersService不再 import 任何业务消费服务新增消费者只需新增一个 Listener无需改动既有代码——这正好符合本仓库技能体系中 arch-open-closed.md 同族规则 所倡导的对扩展开放、对修改关闭精神。3. 消费者通过OnEvent订阅每个模块内部实现自己的 Listener用OnEvent(order.created)装饰器声明订阅的事件名Injectable() export class InventoryListener { OnEvent(order.created) async handleOrderCreated(event: OrderCreatedEvent): Promisevoid { await this.inventoryService.reserve(event.items); } } Injectable() export class EmailListener { OnEvent(order.created) async handleOrderCreated(event: OrderCreatedEvent): Promisevoid { await this.emailService.sendConfirmation(event.orderId); } } Injectable() export class AnalyticsListener { OnEvent(order.created) async handleOrderCreated(event: OrderCreatedEvent): Promisevoid { await this.analyticsService.track(order_created, { orderId: event.orderId, total: event.total, }); } }每个 Listener 都可以被放在与它职责相同的 Feature Module 中如InventoryModule、NotificationsModule实现一个模块一个关注点的模块边界参见 arch-feature-modules.md 规则。三、事件命名与配置仓库中的真实落地1. 事件命名建议事件名使用点分命名空间dot-separated语义为名词 过去分词动作例如order.created订单已创建user.registered用户已注册course.updated课程已更新点分命名与nestjs/event-emitter的delimiter、wildcard配置天然契合见下文支持按前缀通配订阅如order.*。2. 仓库中的全局注册配置在 RS School App 的 NestJS 后端中事件基础设施已经在应用根模块统一注册见 nestjs/src/app.module.tsEventEmitterModule.forRoot({ delimiter: ., wildcard: true, }),delimiter: .声明事件名的分隔符为点号与order.created这种命名约定一致wildcard: true开启通配符订阅允许监听器用order.*之类的模式订阅一类事件而不是逐个精确匹配。同时依赖声明在 nestjs/package.json 中nestjs/event-emitter: 3.0.1。这意味着仓库当前基于 event-emitter 3.x 的 APIEventEmitter2、OnEvent。3. 仓库对事件驱动思想的延伸定时轮询式 Listener从源码结构看当前仓库尚未在业务模块中使用OnEvent装饰器但已通过EventEmitterModule.forRoot预先搭建好事件驱动基础设施。仓库实际的跨系统同步采用定时触发模式nestjs/src/listeners/course.listener.ts中的CourseListener通过Cron(CronExpression.EVERY_30_MINUTES)每 30 分钟查询未完成课程对比 S3 中app/courses.json的内容发生变化时写入 S3 并通过 GitHub API 向站点仓库派发course.updated事件course.listener.ts触发 rs.school 站点重建。这展示了事件思想在跨仓库协作中的应用——通过事件dispatch解耦数据源与站点构建方。该模块在 listeners.module.ts 中以独立 Feature Module 注册providers: [CourseListener]并配套了 course.listener.spec.ts 测试印证监听器独立成模块、可单独测试的组织方式。四、服务间通信消息代理的定位arch-use-events.md规则明确区分了两层事件场景技术选型说明服务内部intra-servicenestjs/event-emitter进程内事件同步/异步均可零外部依赖适合单体模块解耦服务之间inter-service消息代理如 RabbitMQ、Kafka跨进程可靠投递、持久化、消费组、重试适合微服务架构选择依据如果事件只在单个应用进程内流转例如订单创建后扣库存nestjs/event-emitter足够且最轻量如果事件需要被多个独立部署的服务消费、需要离线重放或削峰则应引入消息代理。两种方式可以共存——服务内先经EventEmitter2分发再由专门的桥接层把关键事件转发到消息代理实现渐进式演进。五、最佳实践小结结合规则文档与仓库现状落地事件驱动架构时建议遵循以下要点发布者只依赖EventEmitter2不依赖任何具体消费者新增行为 新增 Listener 注册到对应 Feature Module零改动既有代码事件类用 readonly 字段封装完整上下文id、userId、items、total 等避免消费者反向查询数据库造成耦合事件名统一采用点分命名空间并开启wildcard: true支持按前缀订阅本仓库已在 app.module.ts 中如此配置Listener 遵循单一职责一个 Listener 只处理一类消费逻辑并放在与职责匹配的模块内为每个 Listener 编写单元测试参考仓库中 course.listener.spec.ts 的写法注入 mock 依赖验证事件处理逻辑跨服务场景升级为消息代理不要用进程内 EventEmitter 强行承担分布式职责。参考规则原文arch-use-events.md根模块事件配置nestjs/src/app.module.ts依赖版本声明nestjs/package.json定时事件同步示例nestjs/src/listeners/course.listener.ts监听器模块组织nestjs/src/listeners/listeners.module.ts监听器测试nestjs/src/listeners/course.listener.spec.ts关联架构规则arch-feature-modules.md、arch-single-responsibility.md、arch-avoid-circular-deps.md【免费下载链接】rsschool-appAn application for the RS School education process项目地址: https://gitcode.com/gh_mirrors/rs/rsschool-app创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表