浅谈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 相关文章推荐
Javascript 继承机制实例
Aug 12 Javascript
替代window.event.srcElement效果的可兼容性的函数
Dec 18 Javascript
jquery入门——事件机制之事件中的冒泡现象示例解释
Sep 12 Javascript
ajax与302响应代码测试
Oct 23 Javascript
javascript中的原型链深入理解
Feb 24 Javascript
JQuery弹出层示例可自定义
May 19 Javascript
vue router学习之动态路由和嵌套路由详解
Sep 21 Javascript
React Form组件的实现封装杂谈
May 07 Javascript
解决vue动态为数据添加新属性遇到的问题
Sep 18 Javascript
Layui数据表格 前后端json数据接收的方法
Sep 19 Javascript
element-ui树形控件后台返回的数据+生成组织树的工具类
Mar 05 Javascript
详解Vue3 Teleport 的实践及原理
Dec 02 Vue.js
浅谈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以ROOT权限执行系统命令的方法
2011/02/10 PHP
迪菲-赫尔曼密钥交换(Diffie?Hellman)算法原理和PHP实现版
2015/05/12 PHP
php实现批量修改文件名称的方法
2016/07/23 PHP
php 广告点击统计代码(php+mysql)
2018/02/21 PHP
Yii Framework框架使用PHPExcel组件的方法示例
2019/07/24 PHP
zeroclipboard复制到剪切板的flash
2010/08/04 Javascript
document.getElementBy(&quot;id&quot;)与$(&quot;#id&quot;)有什么区别
2013/09/22 Javascript
JavaScript如何从listbox里同时删除多个项目
2013/10/12 Javascript
Node.js中require的工作原理浅析
2014/06/24 Javascript
加随机数引入脚本不让浏览器读取缓存
2014/09/04 Javascript
讲解JavaScript中for...in语句的使用方法
2015/06/03 Javascript
javaScript事件学习小结(四)event的公共成员(属性和方法)
2016/06/09 Javascript
JSON对象 详解及实例代码
2016/10/18 Javascript
jQuery如何跳转到另一个网页 就这么简单
2016/12/28 Javascript
Javascript Event(事件)的传播与冒泡
2017/01/23 Javascript
微信小程序实现移动端滑动分页效果(ajax)
2017/06/13 Javascript
redux-saga 初识和使用
2018/03/10 Javascript
详解使用Nuxt.js快速搭建服务端渲染(SSR)应用
2019/03/13 Javascript
vue 项目引入echarts 添加点击事件操作
2020/09/09 Javascript
python绘制铅球的运行轨迹代码分享
2017/11/14 Python
Python之reload流程实例代码解析
2018/01/29 Python
Python比较2个时间大小的实现方法
2018/04/10 Python
Python3.8对可迭代解包的改进及用法详解
2019/10/15 Python
Python3实现配置文件差异对比脚本
2019/11/18 Python
python+tifffile之tiff文件读写方式
2020/01/13 Python
CSS3中动画属性transform、transition和animation属性的区别
2016/09/25 HTML / CSS
加拿大建筑和装修专家:Reno-Depot
2017/12/21 全球购物
材料专业毕业生求职信
2014/02/26 职场文书
白血病募捐倡议书
2014/05/14 职场文书
民族学专业职业生涯规划范文:积跬步以至千里
2014/09/11 职场文书
2014年小学教师工作自我评价
2014/09/22 职场文书
网络工程专业大学生求职信
2014/10/01 职场文书
党员四风问题个人对照检查材料
2014/10/26 职场文书
商超业务员岗位职责
2015/02/13 职场文书
小学生心理健康活动总结
2015/05/08 职场文书
Python实现提取PDF简历信息并存入Excel
2022/04/02 Python