中间件
Node.js 中最独特的模式之一无疑是中间件。 不幸的是,对于没有经验的人来说,这也是最令人困惑的问题之一,特别是对于来自企业编程世界的开发人员来说。 迷失方向的原因可能与中间件一词的传统含义有关,在企业架构术语中,中间件代表有助于抽象较低级别机制(例如操作系统 API、网络通信、内存管理等)的各种软件套件。 允许开发人员仅关注应用程序的业务案例。 在这种情况下,中间件一词让人想起 CORBA、企业服务总线、Spring、JBoss 和 WebSphere 等主题,但从其更一般的含义来看,它还可以定义任何类型的软件层,充当较低级别服务和服务之间的粘合剂。 应用程序(字面意思是中间的软件)。
Express 中间件
Express (nodejsdp.link/express) 在 Node.js 世界中普及了中间件这一术语,并将其与非常具体的设计模式绑定在一起。 事实上,在 Express 中,中间件代表一组服务(通常是函数),它们组织在管道中,负责处理传入的 HTTP 请求和相关响应。
Express 以其非常中立和极简主义的 Web 框架而闻名,而中间件模式是其主要原因。 事实上,Express 中间件是一种有效的策略,允许开发人员轻松创建和分发可以轻松添加到应用程序的新功能,而无需扩展框架的简约核心。
Express 中间件具有以下签名:
function (req, res, next) { ... }
这里,req 是传入的 HTTP 请求,res 是响应,next 是当前中间件完成其任务时要调用的回调,并依次触发管道中的下一个中间件。
Express 中间件执行的任务示例如下:
-
解析请求正文
-
压缩/解压缩请求和响应
-
生成访问日志
-
管理会话
-
管理加密 cookie
-
提供跨站点请求伪造 (CSRF) 保护
如果我们仔细想想,这些任务与应用程序的主要业务逻辑并不严格相关,也不是 Web 服务器最小核心的重要组成部分。 它们是附件、组件,为应用程序的其余部分提供支持,并允许实际的请求处理程序仅关注其主要业务逻辑。 本质上,这些任务是 “中间的软件”。
中间件作为一个模式
用于在 Express 中实现中间件的技术并不新鲜,事实上,它可以被视为拦截过滤器模式和责任链模式的 Node.js 化身。 用更通用的术语来说,它还代表一个处理管道,这让我们想起了流。 如今,在 Node.js 中,“中间件” 一词的使用远远超出了 Express 框架的范围,它表示一种特定的模式,其中一组处理单元、过滤器和处理程序以函数的形式连接起来形成异步 序列以便执行任何类型数据的预处理和后处理。 这种模式的主要优点是灵活性。 事实上,中间件模式使我们能够以极少的努力获得插件基础设施,提供一种使用新过滤器和处理程序扩展系统的不引人注目的方式。
如果您想了解有关拦截过滤器模式的更多信息,以下文章是一个很好的起点:nodejsdp.link/intercepting-filter。 同样,责任链模式的详细概述可在以下 URL 中找到:nodejsdp.link/chain-of-responsibility。 |
下图显示了中间件模式的组件:
/image-2024-05-07-16-57-48-093.png)
该模式的基本组件是中间件管理器,它负责组织和执行中间件功能。 该模式最重要的实现细节如下:
-
可以通过调用 use() 函数来注册新的中间件(该函数的名称是中间件模式的许多实现中的通用约定,但我们可以选择任何名称) 。 通常,新的中间件只能附加在管道的末尾,但这不是严格的规则。
-
当收到新数据进行处理时,会在异步顺序执行流中调用已注册的中间件。 管道中的每个单元接收前一个单元的执行结果作为输入。
-
每个中间件都可以决定停止进一步处理数据。 这可以通过调用特殊函数、不调用回调(如果中间件使用回调)或传播错误来完成。 错误情况通常会触发专门用于处理错误的另一个中间件序列的执行。
对于如何在管道中处理和传播数据,没有严格的规则。 在管道中传播数据修改的策略包括:
-
使用附加属性或函数来增强作为输入接收的数据
-
保持数据的不变性并始终返回新的副本作为处理结果
正确的方法取决于数据修改的方式 中间件管理器是根据中间件本身执行的处理类型来实现的。
为 ZeroMQ 创建一个中间件框架
现在让我们通过围绕 ZeroMQ (nodejsdp.link/zeromq) 消息传递库构建一个中间件框架来演示该模式。 ZeroMQ(也称为 ZMQ 或 ØMQ)提供了一个简单的接口,用于使用各种协议在网络上交换原子消息。 它因其性能而引人注目,其基本抽象集是专门为促进自定义消息传递架构的实现而构建的。 因此,经常选择 ZeroMQ 来构建复杂的分布式系统。
在第 13 章 “消息传递和集成模式” 中,我们将有机会更详细地分析 ZeroMQ 的功能。 |
ZeroMQ 的接口非常低级,因为它只允许我们使用字符串和二进制缓冲区来存储消息。 因此,任何数据编码或自定义格式都必须由库的用户来实现。
在下一个示例中,我们将构建一个中间件基础设施来抽象通过 ZeroMQ 套接字传递的数据的预处理和后处理,以便我们可以透明地使用 JSON 对象,而且还可以无缝压缩通过网络传输的消息。
中间件管理器
围绕 ZeroMQ 构建中间件基础设施的第一步是创建一个组件,负责在接收或发送新消息时执行中间件管道。 为此,我们创建一个名为 zmqMiddlewareManager.js 的新模块并定义它:
export class ZmqMiddlewareManager {
constructor (socket) { // (1)
this.socket = socket
this.inboundMiddleware = []
this.outboundMiddleware = []
this.handleIncomingMessages()
.catch(err => console.error(err))
}
async handleIncomingMessages () { // (2)
for await (const [message] of this.socket) {
await this
.executeMiddleware(this.inboundMiddleware, message)
.catch(err => {
console.error('Error while processing the message', err)
})
}
}
async send (message) { // (3)
const finalMessage = await this
.executeMiddleware(this.outboundMiddleware, message)
return this.socket.send(finalMessage)
}
use (middleware) { // (4)
if (middleware.inbound) {
this.inboundMiddleware.push(middleware.inbound)
}
if (middleware.outbound) {
this.outboundMiddleware.unshift(middleware.outbound)
}
}
async executeMiddleware (middlewares, initialMessage) { // (5)
let message = initialMessage
for await (const middlewareFunc of middlewares) {
message = await middlewareFunc.call(this, message)
}
return message
}
}
让我们详细讨论如何实现 ZmqMiddlewareManager:
-
在类的第一部分中,我们定义接受 ZeroMQ 套接字作为参数的构造函数。 在构造函数中,我们创建两个包含中间件函数的空列表,一个用于入站消息,另一个用于出站消息。 接下来,我们立即开始处理来自套接字的消息。 我们在 handleIncomingMessages() 方法中执行此操作。
-
在 handleIncomingMessages() 方法中,我们使用 ZeroMQ 套接字作为异步迭代,并使用 for wait…of 循环,处理任何传入消息并将其传递到中间件的 inboundMiddleware 列表。
-
与 handleIncomingMessages() 类似,send() 方法会将收到的消息作为参数沿着 outboundMiddleware 管道传递。 处理的结果存储在 finalMessage 变量中,然后通过套接字发送。
-
use() 方法用于将新的中间件函数附加到我们的内部管道中。 在我们的实现中,每个中间件都是成对出现的; 它是一个包含两个属性的对象:入站和出站。 每个属性可用于定义要添加到相应列表的中间件函数。 这里值得注意的是,入站中间件被推到 inboundMiddleware 列表的末尾,而出站中间件被插入(使用 unshift())到 outboundMiddleware 列表的开头。 这是因为互补的入站/出站中间件功能通常需要以相反的顺序执行。 例如,如果我们想要使用 JSON 解压缩然后反序列化入站消息,则意味着对于出站消息,我们应该先序列化然后压缩。 这种成对组织中间件的约定严格来说并不是通用模式的一部分,而只是我们特定示例的实现细节。
-
最后一个方法,executeMiddleware(),代表了我们组件的核心,因为它是负责执行中间件功能的部分。 作为输入接收到的中间件数组中的每个函数都被一个接一个地执行,并且中间件函数的执行结果被传递到下一个中间件函数。 请注意,我们对每个中间件函数返回的每个结果使用 await 指令; 这允许中间件函数使用 Promise 同步以及异步返回值。 最后,将最后一个中间件函数的结果返回给调用者。
为简洁起见,我们不支持错误中间件管道。 通常,当中间件函数传播错误时,会执行另一组专门用于处理错误的中间件函数。 这可以使用我们在这里演示的相同技术轻松实现。 例如,除了 inboundMiddleware 和 outboundMiddleware 之外,我们还可以接受额外的(可选)errorMiddleware 函数。 |
实现中间件来处理消息
现在我们已经实现了中间件管理器,我们可以创建第一对中间件函数来演示如何处理入站和出站消息。 正如我们所说,我们的中间件基础设施的目标之一是拥有一个可以序列化和反序列化 JSON 消息的过滤器。 因此,让我们创建一个新的中间件来处理这个问题。 在名为 jsonMiddleware.js 的新模块中,我们包含以下代码:
export const jsonMiddleware = function () {
return {
inbound (message) {
return JSON.parse(message.toString())
},
outbound (message) {
return Buffer.from(JSON.stringify(message))
}
}
}
中间件的入站部分反序列化作为输入接收到的消息,而出站部分将数据序列化为字符串,然后将其转换为缓冲区。
以类似的方式,我们可以在名为 zlibMiddleware.js 的文件中实现一对中间件函数,以使用 zlib 核心模块 (nodejsdp.link/zlib) 来膨胀/收缩消息:
import { inflateRaw, deflateRaw } from 'zlib'
import { promisify } from 'util'
const inflateRawAsync = promisify(inflateRaw)
const deflateRawAsync = promisify(deflateRaw)
export const zlibMiddleware = function () {
return {
inbound (message) {
return inflateRawAsync(Buffer.from(message))
},
outbound (message) {
return deflateRawAsync(message)
}
}
}
与 JSON 中间件相比,我们的 zlib 中间件函数是异步的,并返回一个 Promise 作为结果。 正如我们所知,我们的中间件管理器完全支持这一点。
您可以注意到我们的框架使用的中间件与 Express 中使用的中间件有很大不同。 这是完全正常的,完美地展示了我们如何调整这种模式来满足我们的特定需求。
使用 ZeroMQ 中间件框架
我们现在准备使用刚刚创建的中间件基础设施。 为此,我们将构建一个非常简单的应用程序,客户端定期向服务器发送 ping,服务器回显收到的消息。
从实现的角度来看,我们将依赖于使用 ZeroMQ (nodejsdp.link/zmq-req-rep) 提供的 req/rep 套接字对的请求/回复消息传递模式。 然后,我们将使用 ZmqMiddlewareManager 包装套接字,以获得我们构建的中间件基础设施的所有优势,包括用于序列化/反序列化 JSON 消息的中间件。
我们将在第 13 章 “消息传递和集成模式” 中分析请求/答复模式和其他消息传递模式。 |
服务端
让我们首先在名为 server.js 的文件中创建应用程序的服务器端:
import zeromq from 'zeromq' // (1)
import { ZmqMiddlewareManager } from './zmqMiddlewareManager.js'
import { jsonMiddleware } from './jsonMiddleware.js'
import { zlibMiddleware } from './zlibMiddleware.js'
async function main () {
const socket = new zeromq.Reply() // (2)
await socket.bind('tcp://127.0.0.1:5000')
const zmqm = new ZmqMiddlewareManager(socket) // (3)
zmqm.use(zlibMiddleware())
zmqm.use(jsonMiddleware())
zmqm.use({ // (4)
async inbound (message) {
console.log('Received', message)
if (message.action === 'ping') {
await this.send({ action: 'pong', echo: message.echo })
}
return message
}
})
console.log('Server started')
}
main()
我们的应用程序的服务器端工作如下:
-
我们首先加载必要的依赖项。 ZeroMQ 包本质上是原生 ZeroMQ 库上的 JavaScript 接口。 请参阅nodejsdp.link/npm-zeromq。
-
接下来,在 main() 函数中,我们创建一个新的 ZeroMQ Reply 套接字并将其绑定到本地主机上的端口 5000。
-
然后是我们用中间件管理器包装 ZeroMQ 的部分,然后添加 zlib 和 JSON 中间件。
-
最后,我们准备好处理来自客户端的请求。 我们将通过简单地添加另一个中间件来实现这一点,这次将其用作请求处理程序。
由于我们的请求处理程序位于 zlib 和 JSON 中间件之后,因此我们将收到所接收消息的解压缩和反序列化版本。 另一方面,传递给 send() 的任何数据都将由出站中间件处理,在我们的例子中,它将序列化然后压缩数据。
客户端
在我们的小应用程序的客户端,在一个名为 client.js 的文件中,我们将包含以下代码:
import zeromq from 'zeromq'
import { ZmqMiddlewareManager } from './zmqMiddlewareManager.js'
import { jsonMiddleware } from './jsonMiddleware.js'
import { zlibMiddleware } from './zlibMiddleware.js'
async function main () {
const socket = new zeromq.Request() // (1)
await socket.connect('tcp://127.0.0.1:5000')
const zmqm = new ZmqMiddlewareManager(socket)
zmqm.use(zlibMiddleware())
zmqm.use(jsonMiddleware())
zmqm.use({
inbound (message) {
console.log('Echoed back', message)
return message
}
})
setInterval(() => { // (2)
zmqm.send({ action: 'ping', echo: Date.now() })
.catch(err => console.error(err))
}, 1000)
console.log('Client connected')
}
main()
客户端应用程序的大部分代码与服务器的代码非常相似。 显着的区别是:
-
我们创建请求套接字,而不是回复套接字,并将其连接到远程(或本地)主机,而不是将其绑定到本地端口。 中间件设置的其余部分与服务器中的完全相同,除了我们的请求处理程序现在只打印它收到的任何消息这一事实。 这些消息应该是对我们 ping 请求的 pong 回复。
-
客户端应用程序的核心逻辑是一个每秒发送一条 ping 消息的计时器。
现在,我们准备尝试我们的客户端/服务器对并查看应用程序的运行情况。 首先,启动服务器:
node server.js
然后我们可以使用以下命令在另一个终端中启动客户端:
node client.js
此时,我们应该看到客户端发送消息并且服务器回显消息。
我们的中间件框架完成了它的工作。 它允许我们透明地解压缩/压缩和反序列化/序列化我们的消息,让处理程序可以自由地专注于他们的业务逻辑。
in the wild
我们在本节开头说,在 Node.js 中普及中间件模式的库是 Express (nodejsdp.link/express)。 因此,我们可以轻松地说 Express 也是中间件模式中最著名的示例。
另外两个有趣的例子是:
-
Koa (nodejsdp.link/koa),被称为 Express 的继任者。 它是由 Express 背后的同一团队创建的,并且与其共享理念和主要设计原则。 Koa 的中间件与 Express 的中间件略有不同,因为它使用 async/await 等现代编程技术而不是回调。
-
Middy (nodejsdp.link/middy) 是中间件模式应用于不同于Web 框架的典型示例。 事实上,Middy 是 AWS Lambda 函数的中间件引擎。
接下来,我们将探索命令模式,正如我们很快就会看到的,它是一种非常灵活且多种形式的模式。