浅谈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 相关文章推荐
xss文件页面内容读取(解决)
Nov 28 Javascript
JavaScript获取XML数据附示例截图
Mar 05 Javascript
jquery拖拽效果完整实例(附demo源码下载)
Jan 14 Javascript
JavaScript黑洞数字之运算路线查找算法(递归算法)实例
Jan 28 Javascript
AngularJS入门教程之AngularJS模型
Apr 18 Javascript
jQuery验证插件validate使用详解
May 11 Javascript
使用Bootstrap框架制作查询页面的界面实例代码
May 27 Javascript
省市联动效果的简单实现代码(推荐)
Jun 06 Javascript
canvas 画布在主流浏览器中的尺寸限制详细介绍
Dec 15 Javascript
AngularJS 购物车全选/取消全选功能的实现方法
Aug 14 Javascript
Node.JS枚举统计当前文件夹和子目录下所有代码文件行数
Aug 23 Javascript
JavaScript中ES6规范中let和const的用法和区别
Aug 06 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
php中数据的批量导入(csv文件)
2006/10/09 PHP
Php部分常见问题总结
2006/10/09 PHP
PHP配置心得包含MYSQL5乱码解决
2006/11/20 PHP
php基础教程 php内置函数实例教程
2012/08/21 PHP
php递归函数中使用return的注意事项
2014/01/17 PHP
通过curl模拟post和get方式提交的表单类
2014/04/23 PHP
详解php实现页面静态化原理
2017/06/21 PHP
用js实现控制内容的向上向下滚动效果
2007/06/26 Javascript
JS 有趣的eval优化输入验证实例代码
2013/09/22 Javascript
Ajax中解析Json的两种方法对比分析
2015/06/25 Javascript
使用JQuery FancyBox插件实现图片展示特效
2015/11/16 Javascript
jquery if条件语句的写法
2016/05/19 Javascript
vue 动态改变静态图片以及请求网络图片的实现方法
2018/02/07 Javascript
vue源码解析之事件机制原理
2018/04/21 Javascript
详解Vue.js使用Swiper.js在iOS
2018/09/10 Javascript
@angular前端项目代码优化之构建Api Tree的方法
2018/12/24 Javascript
JavaScript实现的九种排序算法
2019/03/04 Javascript
原生JS实现随机点名项目的实例代码
2019/04/30 Javascript
解决echarts echarts数据动态更新和dataZoom被重置问题
2020/07/20 Javascript
[02:14]DOTA2英雄基础教程 修补匠
2013/12/23 DOTA
Python脚本实现格式化css文件
2015/04/08 Python
Python简单实现自动删除目录下空文件夹的方法
2017/08/29 Python
python实现k-means聚类算法
2018/02/23 Python
python使用webdriver爬取微信公众号
2018/08/31 Python
对pandas的层次索引与取值的新方法详解
2018/11/06 Python
opencv实现图片模糊和锐化操作
2018/11/19 Python
对pandas处理json数据的方法详解
2019/02/08 Python
Python模块future用法原理详解
2020/01/20 Python
webapp字号大小跟随系统字号大小缩放的示例代码
2018/12/26 HTML / CSS
项目经理任命书内容
2014/06/06 职场文书
民事和解协议书格式
2014/11/29 职场文书
孔庙导游词
2015/02/04 职场文书
2015初中教导处工作总结
2015/07/21 职场文书
Python下opencv库的安装过程及问题汇总
2021/06/11 Python
python入门学习关于for else的特殊特性讲解
2021/11/20 Python
css之clearfix的用法深入理解(必看篇)
2023/05/21 HTML / CSS