【原创】NodeJS中间件机制学习

理解NodeJS中间件机制核心代码的实现,加深对中间件机制的理解,有助于更好的使用和编写中间件。

目录

  • 中间件概念
  • 中间件机制核心实现
  • 中间件社区

中间件概念

在NodeJS中,中间件主要是指封装所有Http请求细节处理的方法。一次Http请求通常包含很多工作,如记录日志、ip过滤、查询字符串、请求体解析、Cookie处理、权限验证、参数验证、异常处理等,但对于Web应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。

中间件的行为比较类似Java中过滤器的工作原理,就是在进入具体的业务处理之前,先让过滤器处理。它的工作模型下图所示。

中间件工作模型

中间件机制核心实现

中间件是从Http请求发起到响应结束过程中的处理方法,通常需要对请求和响应进行处理,因此一个基本的中间件的形式如下:

const middleware = (req, res, next) => {
  // TODO
  next()
}

以下通过两种方式的中间件机制的实现来理解中间件是如何工作的。

方式一

如下定义三个简单的中间件:

const middleware1 = (req, res, next) => {
  console.log('middleware1 start')
  next()
}

const middleware2 = (req, res, next) => {
  console.log('middleware2 start')
  next()
}

const middleware3 = (req, res, next) => {
  console.log('middleware3 start')
  next()
}

通过递归的形式,将后续中间件的执行方法传递给当前中间件,在当前中间件执行结束,通过调用next()方法执行后续中间件的调用。

// 中间件数组
const middlewares = [middleware1, middleware2, middleware3]
function run (req, res) {
  const next = () => {
    // 获取中间件数组中第一个中间件
    const middleware = middlewares.shift()
    if (middleware) {
      middleware(req, res, next)
    }
  }
  next()
}
run() // 模拟一次请求发起

执行以上代码,可以看到如下结果:

middleware1 start
middleware2 start
middleware3 start

如果中间件中有异步操作,需要在异步操作的流程结束后再调用next()方法,否则中间件不能按顺序执行。改写middleware2中间件:

const middleware2 = (req, res, next) => {
  console.log('middleware2 start')
  new Promise(resolve => {
    setTimeout(() => resolve(), 1000)
  }).then(() => {
    next()
  })
}

执行结果与之前一致,不过middleware3会在middleware2异步完成后执行。

执行结果

方式二

有些中间件不止需要在业务处理前执行,还需要在业务处理后执行,比如统计时间的日志中间件。在方式一情况下,无法在next()为异步操作时再将当前中间件的其他代码作为回调执行。因此可以将next()方法的后续操作封装成一个Promise对象,中间件内部就可以使用next.then()形式完成业务处理结束后的回调。改写run()方法如下:

function run (req, res) {
  const next = () => {
    const middleware = middlewares.shift()
    if (middleware) {
      // 将middleware(req, res, next)包装为Promise对象
      return Promise.resolve(middleware(req, res, next))
    }
  }
  next()
}

中间件的调用方式需改写为:

const middleware1 = (req, res, next) => {
  console.log('middleware1 start')
  // 所有的中间件都应返回一个Promise对象
  // Promise.resolve()方法接收中间件返回的Promise对象,供下层中间件异步控制
  return next().then(() => {
    console.log('middleware1 end')
  })
}

得益于async函数的自动异步流程控制,中间件也可以用如下方式来实现:

// async函数自动返回Promise对象
const middleware2 = async (req, res, next) => {
  console.log('middleware2 start')
  await new Promise(resolve => {
    setTimeout(() => resolve(), 1000)
  })
  await next()
  console.log('middleware2 end')
}

const middleware3 = async (req, res, next) => {
  console.log('middleware3 start')
  await next()
  console.log('middleware3 end')
}

执行结果如下:

执行结果

以上描述了中间件机制中多个异步中间件的调用流程,实际中间件机制的实现还需要考虑异常处理、路由等。

express框架中,中间件的实现方式为方式一,并且全局中间件和内置路由中间件中根据请求路径定义的中间件共同作用,不过无法在业务处理结束后再调用当前中间件中的代码。koa2框架中中间件的实现方式为方式二,将next()方法返回值封装成一个Promise,便于后续中间件的异步流程控制,实现了koa2框架提出的洋葱圈模型,即每一层中间件相当于一个球面,当贯穿整个模型时,实际上每一个球面会穿透两次。

koa2中间件洋葱圈模型

koa2框架的中间件机制实现得非常简洁和优雅,这里学习一下框架中组合多个中间件的核心代码。

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!')
  }
  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      // index会在next()方法调用后累加,防止next()方法重复调用
      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 {
        // 核心代码
        // 包装next()方法返回值为Promise对象
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        // 遇到异常中断后续中间件的调用
        return Promise.reject(err)
      }
    }
  }
}

中间件社区

在后续NodeJS学习和应用中,建议使用koa2框架作为基础框架,这里列出了一些使用比较多的中间件。

koa中间件列表地址:https://github.com/koajs/koa/wiki

中间件的具体使用方式还请小伙伴们自行查询官方文档。

总结

本文主要介绍了中间件的概念、为何引入中间件以及中间件机制的核心实现。中间件机制使得Web应用具备良好的可扩展性和组合性。

在实现中间件时,单个中间件应该足够简单,职责单一。由于每个请求都会调用中间件相关代码,中间件的代码应该高效,必要的时候可以缓存重复获取的数据。在对不同的路由使用中间件时,还应该考虑到不同的中间件应用到不同的路由上。

本文参考资源如下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容