在刚刚我们说明了中间件是「请求—响应」管道上的一环,通过把横切逻辑拆成小函数、按顺序组合成一条链,实现了可插拔与可复用。这种「拆成小单元、再组合」的思路,在 Node 里随处可见,背后对应着一类常见的设计模式。
这部分我们从设计模式的视角看 Node 中的三种典型写法:模块化与解耦、事件发布/订阅以及中间件与组合模式。我们会说明为何要按模块划分边界、依赖方向如何影响解耦; 接着介绍 Node 内置的 EventEmitter 与发布/订阅如何解耦生产者与消费者;最后把中间件管道抽象成「组合模式」或「责任链」的一种实现,与前一讲的管道概念衔接起来,形成更完整的设计视角。
当项目从几个文件长成几十个路由、多处复用同一段逻辑时,若所有代码都堆在少数几个文件里,修改一处就可能牵动全局,测试和复用也会变得困难。模块化的目标,是把「职责相近、会一起变化」的代码收拢到同一模块里,把「依赖方向」控制在单向:例如路由层只依赖 Controller,Controller 只依赖服务层,服务层不依赖 HTTP 或路由。这样,改路由表不会影响服务层实现,改服务层逻辑也不会倒过来要求改路由。
与此相关的是目录与职责划分。在 Node 里,我们通常按「层」或「功能」来组织目录:例如 routes/ 只放路由注册、controllers/ 只放「入参 → 调服务 → 选状态码与响应」的薄层、services/ 放业务逻辑、utils/ 或 lib/ 放与业务无关的通用工具。每一层只依赖更内层或同层,不反向依赖;这样在写单元测试时,可以只 mock 服务层来测 Controller,而不必启动整个 HTTP 服务。
依赖方向稳定之后,接口就会自然收敛:Controller 只关心「服务层暴露什么方法、返回什么结构」,不关心服务层内部是查库还是调远程接口。这种「面向接口、背对实现」的写法,正是解耦的一种体现。下一节我们看另一种在 Node 里极为常见的解耦方式:事件发布/订阅。

Node 内置的 events 模块提供了 EventEmitter:一个对象可以「发出」具名事件,其它对象可以「订阅」这些事件并在发生时执行回调。这样,发出事件的一方不需要知道谁在监听、有多少个监听器;监听的一方也不需要知道事件是谁发出的、在什么时机发出。双方只通过「事件名 + 载荷」这种约定耦合,从而在结构上解耦。
这种模式叫发布/订阅(publish-subscribe):生产者只负责 emit('eventName', data),消费者只负责 on('eventName', (data) => { ... })。在 Node 里,很多内置对象都是 EventEmitter 的实例或子类:例如 http.Server 会发出 request、error、close 等事件,流会发出 data、end、error;我们写的业务逻辑也常常继承 EventEmitter,在合适的时机发出「订单已创建」「用户已登录」等领域事件,由其它模块订阅并执行副作用(写日志、发通知、更新缓存)。
与事件循环的关系在于:emit 会同步调用当前注册在该事件上的所有监听器;监听器若包含异步操作,不会阻塞 emit 返回,但异步完成后的回调会进入事件循环的任务队列。因此,若在监听器里做了耗时操作,要注意不要阻塞主线程;若希望「发事件」与「处理事件」完全解耦,可以只在监听器里把任务推入队列,由另一层逻辑异步消费。
下面这段代码演示如何用 EventEmitter 解耦「用户注册」与「发送欢迎邮件」:注册逻辑只负责写库并发出 user:registered 事件,邮件逻辑只订阅该事件并发送邮件,两者互不直接依赖。
|const EventEmitter = require('events'); class UserService extends EventEmitter { register(name, email) { // 写库等逻辑省略 const user = { id: 1, name, email }; this.emit('user:registered', user); return user; } } const userService = new UserService(); userService.on('user:registered', (user) => { console.log(`发送欢迎邮件给 ${user.email}`); }); userService.register('Alice', 'alice@example.com');

在第十一讲中,我们把中间件理解为「管道上的一环」:请求依次经过多个中间件,每个中间件要么把控制交给下一环,要么直接结束响应。从设计模式的角度看,这条管道正是组合的一种体现:多个小函数(每个中间件)被「组合」成一条链,请求从链头进入、从某一段或链尾得到响应。每个小函数只关心「自己这一环做什么、何时调用 next」,不关心整条链有多长、前后还有哪些环;这种「同一接口、可替换可叠加」的写法,与组合模式或责任链模式的精神一致。
与此相关的是与第 11 讲概念的对应。本讲的「管道」即一条由 (ctx, next) 或 (req, res, next) 组成的链;next 的调用相当于「把控制交给下一个处理者」,若某一环不调用 next 也不结束响应,链就在这一环挂起。Express 的线性链与 Koa 的洋葱链,都是这种「组合多个处理者」的不同实现:Express 是单向传递、Koa 在返回路径上再执行一遍「next 之后」的代码。因此,当你写 app.use(middleware) 时,本质上是在往这条组合链上追加一环;理解这一点,有助于在更复杂的架构里(例如网关、BFF)复用同一套「可插拔、可组合」的思路。
中间件与组合模式的关系在于:每个中间件是「处理者」的一个实现,管道是「把多个处理者串成链」的容器;新增或替换某一环,不需要改动其它环,符合开放封闭。下一讲我们会讨论错误处理与服务健壮性,届时会再看到「统一错误处理中间件」如何作为链上的一环,与本节的设计视角呼应。

模块化与解耦、事件发布/订阅、中间件与组合模式,在 Node 里常常一起出现:路由与 Controller 通过模块边界解耦,业务逻辑通过事件与副作用解耦,横切逻辑通过中间件管道组合。理解这三种模式,有助于在更大项目中保持结构清晰、易于测试和扩展。
这节课我们围绕 Node.js 常见设计模式 展开了三块内容。在模块化与解耦中,我们说明了为何要按职责划分模块、依赖方向如何影响解耦,以及目录与层(路由、Controller、服务)的对应关系,并与前面几讲中的 Controller、服务层边界衔接。
理解了这三种模式之后,下一部分我们将讨论 错误处理与服务健壮性:同步错误与异步错误的差异、全局错误处理(未捕获异常、Promise 拒绝)以及为什么 Node 服务会「突然挂掉」,从而在设计和运行时两方面把服务写得更稳。