浅谈Node.js 中间件模式


Posted in Javascript onJune 12, 2018

中间件在 Node.js 中被广泛使用,它泛指一种特定的设计模式、一系列的处理单元、过滤器和处理程序,以函数的形式存在,连接在一起,形成一个异步队列,来完成对任何数据的预处理和后处理。

它的优点在于 灵活性 :使用中间件我们用极少的操作就能得到一个插件,用最简单的方法就能将新的过滤器和处理程序扩展到现有的系统上。

常规中间件模式

中间件模式中,最基础的组成部分就是 中间件管理器 ,我们可以用它来组织和执行中间件的函数,如图所示:

浅谈Node.js 中间件模式

要实现中间件模式,最重要的实现细节是:

  1. 可以通过调用use()函数来注册新的中间件,通常,新的中间件只能被添加到高压包带的末端,但不是严格要求这么做;
  2. 当接收到需要处理的新数据时,注册的中间件在意不执行流程中被依次调用。每个中间件都接受上一个中间件的执行结果作为输入值;
  3. 每个中间件都可以停止数据的进一步处理,只需要简单地不调用它的毁掉函数或者将错误传递给回调函数。当发生错误时,通常会触发执行另一个专门处理错误的中间件。

至于怎么处理传递数据,目前没有严格的规则,一般有几种方式

  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 中使用的中间件

前面展示的中间件模型使用回调函数实现的,但是现在有一个比较时髦的 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`);
}));

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
推荐:极酷右键菜单
Nov 29 Javascript
JavaScript 继承使用分析
May 12 Javascript
$(document).ready(function() {})不执行初始化脚本
Jun 19 Javascript
学习JavaScript设计模式之装饰者模式
Jan 19 Javascript
JS不用正则验证输入的字符串是否为空(包含空格)的实现代码
Jun 14 Javascript
EasyUI Pagination 分页的两种做法小结
Jul 09 Javascript
javascript实现的图片预览功能
Mar 25 Javascript
JS实现图片预览的两种方式
Jun 27 Javascript
Vuex利用state保存新闻数据实例
Jun 28 Javascript
微信小程序实现收货地址左滑删除
Nov 18 Javascript
微信小程序实现音频文件播放进度的实例代码
Mar 02 Javascript
原生js实现无缝轮播图效果
Jan 28 Javascript
浅谈Webpack打包优化技巧
Jun 12 #Javascript
关于TypeScript模块导入的那些事
Jun 12 #Javascript
JS实现前端页面的搜索功能
Jun 12 #Javascript
微信小程序实现弹出菜单功能
Jun 12 #Javascript
微信小程序实现折叠与展开文章功能
Jun 12 #Javascript
微信小程序收藏功能的实现代码
Jun 12 #Javascript
记一次webpack3升级webpack4的踩坑经历
Jun 12 #Javascript
You might like
德生BCL3000的电路分析和打磨
2021/03/02 无线电
本地机apache配置基于域名的虚拟主机详解
2013/08/10 PHP
PHP中使用memcache存储session的三种配置方法
2014/04/05 PHP
php实现高效获取图片尺寸的方法
2014/12/12 PHP
php实现简单文件下载的方法
2015/01/30 PHP
php中mysql操作buffer用法详解
2015/03/19 PHP
JQuery 简便实现页面元素数据验证功能
2007/03/24 Javascript
ECMAScript5中的对象存取器属性:getter和setter介绍
2014/12/08 Javascript
JavaScript中用getDate()方法返回指定日期的教程
2015/06/09 Javascript
AngularJS指令用法详解
2016/11/02 Javascript
vue2实现移动端上传、预览、压缩图片解决拍照旋转问题
2017/04/13 Javascript
详解react-native-fs插件的使用以及遇到的坑
2017/09/12 Javascript
微信小程序实现同时上传多张图片
2020/02/03 Javascript
[01:43]倾听DOTA2英雄之声 魅惑魔女国服配音鉴赏
2013/06/06 DOTA
[42:27]DOTA2上海特级锦标赛主赛事日 - 3 败者组第三轮#2Fnatic VS OG第三局
2016/03/05 DOTA
[02:32]“虐狗”镜头慎点 2016国际邀请赛中国区预选赛现场玩家采访
2016/06/28 DOTA
零基础写python爬虫之urllib2中的两个重要概念:Openers和Handlers
2014/11/05 Python
Python中的复制操作及copy模块中的浅拷贝与深拷贝方法
2016/07/02 Python
numpy数组拼接简单示例
2017/12/15 Python
python中pika模块问题的深入探究
2018/10/13 Python
PyCharm 创建指定版本的 Django(超详图解教程)
2019/06/18 Python
Python求两个字符串最长公共子序列代码实例
2020/03/05 Python
python 将视频 通过视频帧转换成时间实例
2020/04/23 Python
IntelliJ 中配置 Anaconda的过程图解
2020/06/01 Python
Python爬取股票信息,并可视化数据的示例
2020/09/26 Python
Selenium 安装和简单使用的实现
2020/12/04 Python
澳大利亚和新西兰最大的在线旅行社之一:Aunt Betty
2019/08/07 全球购物
学生自我鉴定范文
2013/10/04 职场文书
幼儿园春季开学寄语
2014/04/03 职场文书
讲文明知礼仪演讲稿
2014/09/13 职场文书
个人租房协议书
2014/11/28 职场文书
人与自然的观后感
2015/06/18 职场文书
超级详细实用的pycharm常用快捷键
2021/05/12 Python
解析高可用Redis服务架构分析与搭建方案
2021/06/20 Redis
Python常用配置文件ini、json、yaml读写总结
2021/07/09 Python
Linux在两个服务器直接传文件的操作方法
2022/08/05 Servers