浅谈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 相关文章推荐
动态加载iframe
Jun 16 Javascript
arguments对象
Nov 20 Javascript
jquery 最简单易用的表单验证插件
Feb 27 Javascript
Js中获取frames中的元素示例代码
Jul 30 Javascript
再谈Javascript中的异步以及如何异步
Aug 19 Javascript
JS实现物体带缓冲的间歇运动效果示例
Dec 22 Javascript
微信小程序开发经验总结(推荐)
Jan 11 Javascript
vuejs开发组件分享之H5图片上传、压缩及拍照旋转的问题处理
Mar 06 Javascript
Vue-cli proxyTable 解决开发环境的跨域问题详解
May 18 Javascript
JavaScript使用FileReader实现图片上传预览效果
Mar 27 Javascript
react-native android状态栏的实现
Jun 15 Javascript
10分钟彻底搞懂Http的强制缓存和协商缓存(小结)
Aug 30 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日期时间函数的高级应用技巧
2009/05/16 PHP
PHP基于php_imagick_st-Q8.dll实现JPG合成GIF图片的方法
2014/07/11 PHP
php的GD库imagettftext函数解决中文乱码问题
2015/01/24 PHP
PHP实现一维数组转二维数组的方法
2015/02/25 PHP
PHP的中使用非缓冲模式查询数据库的方法
2017/02/05 PHP
PHP常用操作类之通信数据封装类的实现
2017/07/16 PHP
Laravel 框架返回状态拦截代码
2019/10/18 PHP
js二维数组排序的简单示例代码
2014/01/24 Javascript
javascript去除字符串左右两端的空格
2015/02/05 Javascript
优化RequireJS项目的相关技巧总结
2015/07/01 Javascript
javascript实现网页字符定位的方法
2015/07/14 Javascript
JavaScript实现点击单选按钮改变输入框中文本域内容的方法
2015/08/12 Javascript
JS基础随笔(菜鸟必看篇)
2016/07/13 Javascript
jquery append与appendTo方法比较
2017/05/24 jQuery
使用vue打包时vendor文件过大或者是app.js文件很大的问题
2018/06/29 Javascript
JS数组中对象去重操作示例
2019/06/04 Javascript
Vue 3.0 前瞻Vue Function API新特性体验
2019/08/12 Javascript
浅析Vue中拆分视图层代码的5点建议
2019/08/15 Javascript
javascript/jquery实现点击触发事件的方法分析
2019/11/11 jQuery
JS快速实现简单计算器
2020/04/08 Javascript
vue-router重写push方法,解决相同路径跳转报错问题
2020/08/07 Javascript
Python中函数参数设置及使用的学习笔记
2016/05/03 Python
Python基于matplotlib绘制栈式直方图的方法示例
2017/08/09 Python
python基于http下载视频或音频
2018/06/20 Python
用Pelican搭建一个极简静态博客系统过程解析
2019/08/22 Python
Flask项目中实现短信验证码和邮箱验证码功能
2019/12/05 Python
Python urlencode和unquote函数使用实例解析
2020/03/31 Python
利用python实现凯撒密码加解密功能
2020/03/31 Python
丝芙兰中国官方商城:SEPHORA中国
2018/01/10 全球购物
服装促销活动方案
2014/02/23 职场文书
2014社区三八妇女节活动总结
2014/03/01 职场文书
社区党的群众路线教育实践活动剖析材料
2014/10/09 职场文书
2014年机关党委工作总结
2014/12/11 职场文书
销售助理岗位职责
2015/02/11 职场文书
垂直极限观后感
2015/06/08 职场文书
python实现进度条的多种实现
2021/04/29 Python