Node.js 中间件模式详解:从基础实现到 Koa2 洋葱模型

作者:夜航船时间:2026-10-22 15:22:45浏览:482

本文深入解析 Node.js 中的中间件模式,涵盖其基本概念、灵活性优势及基础实现。通过对比 Express 的回调机制与 Koa2 基于 Promise 和 async/await 的洋葱模型源码,详细阐述中间件执行流程、错误处理及数据传递方式,帮助开发者理解不同框架中间件的设计差异。

Node.js 中间件模式概述

在Node.js生态中,中间件是一种广泛采用的设计模式,表现为一系列串联的函数处理单元或过滤器,它们组成异步队列,负责数据的预处理与后处理。

其核心优势在于灵活性:借助中间件,开发者能以极简的操作创建插件,并以最简便的方式将新的过滤器和处理程序扩展至现有系统。

常规中间件模式实现

常规中间件模式

该模式的基础组件是中间件管理器,用于组织和执行中间件函数,具体结构如图所示:

Node.js 中间件模式概述

实现中间件模式的关键细节包括:

  1. 通过调用use()函数注册新中间件;通常新中间件需添加至队列末端,但这并非强制要求。
  2. 当接收到待处理数据时,注册的中间件按执行流程依次被调用,每个中间件接收前一个中间件的执行结果作为输入。
  3. 中间件可通过不调用next()函数或向回调传递错误来终止后续处理;发生错误时,通常会触发专门处理错误的中间件。

数据传递方式无严格规则,常见处理策略包括:

  1. 通过添加属性和方法来增强对象;
  2. 利用某种处理结果替换data;
  3. 保持原始数据不变,始终返回新副本作为处理结果。

具体处理方式取决于中间件管理器的实现及中间件需完成的任务类型。

消息传递库中间件示例

以下示例源自《Node.js 设计模式 第二版》,展示了为消息传递库实现中间件管理器的方法:


class ZmqMiddlewareManager {
 constructor(socket) {
  this.socket = socket;
  // 两个列表分别保存两类中间件函数:接受到的信息和发送的信息。
  this.inboundMiddleware = [];
  this.outboundMiddleware = [];
  socket.on('message', message => {
   this.executeMiddleware(this.inboundMiddleware, {
    data: message
   });
  });
 }
 
 send(data) {
  const message = { data };
  
  this.excuteMiddleware(this.outboundMiddleware, message, () => {
   this.socket.send(message.data);
  });
 }
 
 use(middleware) {
  if(middleware.inbound) {
   this.inboundMiddleware.push(middleware.inbound);
  }
  if(middleware.outbound) {
   this.outboundMiddleware.push(middleware.outbound);
  }
 }
 
 exucuteMiddleware(middleware, arg, finish) {
  function iterator(index) {
   if(index === middleware.length) {
    return finish && finish();
   }
   middleware[index].call(this, arg, err => {
    if(err) {
     return console.log('There was an error: ' + err.message);
    }
    iterator.call(this, ++index);
   });
  }
  iterator.call(this, 0);
 }
}

随后需创建中间件,在inbound和outbound中定义中间件函数,执行完毕后调用next()即可。例如:


const zmqm = new ZmqMiddlewareManager();

zmqm.use({
 inbound: function(message, next) {
  console.log('input message: ', message.data);
  next();
 },
 outbound: function(message, next) {
  console.log('output message: ', message.data);
  next();
 }
});

Express 中间件机制

Express推广的中间件概念与此类似,其典型形式如下:


function(req, res, next) { ... }

Koa2 中间件与洋葱模型源码解析

Koa2中使用的中间件

前述模型基于回调函数实现,而流行的Node.js框架Koa2的中间件实现有所不同。Koa2移除了早期基于ES2015生成器的实现,兼容回调函数、convert后的生成器以及async和await。

Koa2官方文档提供了中间件的洋葱模型,如图所示:

Node.js 中间件模式概述

图中显示,先进入inbound的中间件函数在outbound中延后执行。为探究原因,需分析Koa2源码。

在koa/lib/applications.js中,构造函数关键部分为this.middleware,它是一个inbound队列:


constructor() {
 super();

 this.proxy = false;
 this.middleware = [];
 this.subdomainOffset = 2;
 this.env = process.env.NODE_ENV || 'development';
 this.context = Object.create(context);
 this.request = Object.create(request);
 this.response = Object.create(response);
}

与前述一致,Koa2使用use()将中间件加入队列:


use(fn) {
 if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
 if (isGeneratorFunction(fn)) {
  deprecate('Support for generators will be removed in v3. ' +
    'See the documentation for examples of how to convert old middleware ' +
    'https://github.com/koajs/koa/blob/master/docs/migration.md');
  fn = convert(fn);
 }
 debug('use %s', fn._name || fn.name || '-');
 this.middleware.push(fn);
 return this;
}

接着框架对端口监听进行了简单封装:


// 封装之前 http.createServer(app.callback()).listen(...)
listen(...args) {
 debug('listen');
 const server = http.createServer(this.callback());
 return server.listen(...args);
}

中间件管理的核心在于this.callback(),该方法实现如下:


callback() {
 const fn = compose(this.middleware);
 
 if (!this.listenerCount('error')) this.on('error', this.onerror);
 
 const handleRequest = (req, res) => {
  const ctx = this.createContext(req, res);
  return this.handleRequest(ctx, fn);
 };
 
 return handleRequest;
}

此处的compose方法是Koa2核心模块koa-compose (https://github.com/koajs/compose)的一部分,封装了中间件执行逻辑:


function compose (middleware) {
 if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
 for (const fn of middleware) {
  if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
 }
 
  /**
  * @param {Object} context
  * @return {Promise}
  * @api public
  */
 
 return function (context, next) {
  // last called middleware #
  let index = -1
  return dispatch(0)
  function dispatch (i) {
   if (i <= index) return Promise.reject(new Error('next() called multiple times'))
   index = i
   let fn = middleware[i]
   if (i === middleware.length) fn = next
   if (!fn) return Promise.resolve()
   try {
    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
   } catch (err) {
    return Promise.reject(err)
   }
  }
 }
}

可见,compose通过递归对中间件队列进行反序遍历,生成Promise链,随后调用Promise即可执行中间件函数:


handleRequest(ctx, fnMiddleware) {
 const res = ctx.res;
 res.statusCode = 404;
 const onerror = err => ctx.onerror(err);
 const handleResponse = () => respond(ctx);
 onFinished(res, onerror);
 return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

源码显示next()返回Promise,因此通用中间件写法为:


app.use((ctx, next) => {
 const start = new Date();
 return next().then(() => {
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
 });
});

若使用async和await,写法亦可行:


app.use((ctx, next) => {
 const start = new Date();
 await next();
 const ms = new Date() - start;
 console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

鉴于许多Koa1项目中间件基于生成器,需使用koa-convert进行平滑升级:


const convert = require('koa-convert');

app.use(convert(function *(next) {
 const start = new Date();
 yield next;
 const ms = new Date() - start;
 console.log(`${this.method} ${this.url} - ${ms}ms`);
}));

本文内容至此结束,希望对读者学习有所帮助,并感谢大家的支持。

作者最新文章