前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >koa与express的中间件机制揭秘

koa与express的中间件机制揭秘

作者头像
挥刀北上
发布2019-07-19 15:15:27
3K0
发布2019-07-19 15:15:27
举报
文章被收录于专栏:Node.js开发Node.js开发

TJ大神开发完express和koa后毅然决然的离开了nodejs转向了go,但这两个web开发框架依然是用nodejs做web开发应用最多的。

koa和express这两个web开发框架都有自己的中间件机制,那这两个机制有什么不同呢?

koa这里我们以koa2为例,koa在问世以来大有席卷express的势头,下面来看一段koa的运行代码:

代码语言:javascript
复制
const Koa = require('koa')

const app = new Koa()

app.use(async function m1 (ctx, next) {
  console.log('m1')
  await next()
  console.log('m1 end')
})

app.use(async function m2 (ctx, next) {
  console.log('m2')
  await next()
  console.log('m2 end')
})

app.use(async function m3 (ctx) {
  console.log('m3')
  ctx.body = 'hello'
})

app.listen(8080)

执行结果为:

代码语言:javascript
复制
m1
m2
m3
m2 end
m1 end

根据这段代码的运行结果于是有人得出结论,Koa的中间件模型: 洋葱形,如图所示:

而对于express有些人说express的中间件是线性执行的,从上到下依次执行,仔细分析这句话好像啥也没说。

接着咱们看一下一段express中间件执行的代码:

代码语言:javascript
复制
const connect = require('express')

const app = connect()

app.use(function m1 (req, res, next) {
  console.log('m1')
  next()
  console.log('m1 end')
})

app.use(function m2 (req, res, next) {
  console.log('m2')
  next()
  console.log('m2 end')
})

app.use(function m3 (req, res, next) {
  console.log('m3')
  res.end('hello')
})

app.listen(8080)

执行结果如下:

代码语言:javascript
复制
m1
m2
m3
m2 end
m1 end

什么情况,彻底懵逼状态,这和koa好像没哈区别吗,express按照这个结果也是洋葱型啊。

先别急,再仔细看一下两段代码,先来看express,按照开发者的思路,在m3中间件中调用了res.send之后,请求-处理-响应这个流程就结束了,但是程序还在执行,为什么会是这个样子呢?这需要了解一下express中间的实现原理,express调用中间件的原理最终运行时是这个样子的,伪代码如下:

代码语言:javascript
复制
app.use(function middleware1(req, res, next) {
    console.log('middleware1 开始')
        // next()
        (function (req, res, next) {
            console.log('middleware2 开始')
                // next()
                (function (req, res, next) {
                    console.log('middleware3 开始')
                        // next()
                        (function handler(req, res, next) {
                            res.send("end")
                            console.log('123456')
                        })()
                    console.log('middleware3 结束')
                })()
            console.log('middleware2 结束')
        })()
    console.log('middleware1 结束')
})

可以看到express的中间件的原理就是一层层函数的嵌套,虽然最内部的函数调用res.send结束的请求,但是程序依然在运行。并且这个运行的结果也类似koa的洋葱。这里面有一点需要注意,express结束请求是在最内部函数。这很重要。

koa的实现主要依赖compose这个函数,接下来咱们看一下这个函数的代码:

代码语言:javascript
复制
// 完整版
function compose (middleware) {
  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
      const fn = middleware[i] || next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
      }))
      } catch (err) {
        return Promise.reject(err)
      }}}
  }

有点长不好看懂,简化之后如下:

代码语言:javascript
复制
// 简化版
function compose(middleware) {
  return function(context, next) {
    let index = -1
    return dispatch(0)
    function dispatch(i) {
      index = i
      const fn = middleware[i] || next
      if (!fn) return Promise.resolve()
      return Promise.resolve(fn(context, function next() {
        return dispatch(i + 1)
      }))
    }
  }
}

一个递归调用,连续调用中间件,以三次为例:代码如下:

第一次,此时第一个中间件被调用,dispatch(0),展开:

代码语言:javascript
复制
Promise.resolve(function(context, next){
  //中间件一第一部分代码
  await/yield next();
  //中间件一第二部分代码
}());

很明显这里的next指向dispatch(1),那么就进入了第二个中间件;

第二次,此时第二个中间件被调用,dispatch(1),展开:

代码语言:javascript
复制
Promise.resolve(function(context, 中间件2){
  //中间件一第一部分代码
  await/yield Promise.resolve(function(context, next){
    //中间件二第一部分代码
    await/yield next();
    //中间件二第二部分代码
  }())
  //中间件一第二部分代码
}());

很明显这里的next指向dispatch(2),那么就进入了第三个中间件;

第三次,此时第二个中间件被调用,dispatch(2),展开:

代码语言:javascript
复制
Promise.resolve(function(context, 中间件2){
  //中间件一第一部分代码
  await/yield Promise.resolve(function(context, 中间件3){
    //中间件二第一部分代码
    await/yield Promise(function(context){
      //中间件三代码
    }());
    //中间件二第二部分代码
  })
  //中间件一第二部分代码
}());

此时中间件三代码执行完毕,开始执行中间件二第二部分代码,执行完毕,开始执行中间一第二部分代码,执行完毕,所有中间件加载完毕。

可以看到,Koa2的中间件机制和express没啥区别,都是回调函数的嵌套,遇到next或者 await next就中断本中间件的代码执行,跳转到对应的下一个中间件执行期内的代码…一直到最后一个中间件,然后逆序回退到倒数第二个中间件await next 或者next下部分的代码执行,完成后继续回退…一直回退到第一个中间件await next或者next下部分的代码执行完成,中间件全部执行结束。

仔细看一下koa除了调用next的时候前面加了一个await好像和express没有任何区别,都是函数嵌套,都是洋葱模型。但是咱们回过头再仔细看一下文章最上面koa的运行代码,koa是在哪里响应的用户请求呢?koa中好型并没有cxt.send这样的函数,只有cxt.body,但是调用cxt.body并不是直接结束请求返回响应啊,和express的res.send有着本质上的不同。下面引用一段其他网友总结的express和koa中间件机制的不同,我个人感觉总结的很到位:

其实中间件执行逻辑没有什么特别的不同,都是依赖函数调用栈的执行顺序,抬杠一点讲都可以叫做洋葱模型。Koa 依靠 async/await(generator + co)让异步操作可以变成同步写法,更好理解。最关键的不是这些中间的执行顺序,而是响应的时机,Express 使用 res.end() 是立即返回,这样想要做出些响应前的操作变得比较麻烦;而 Koa 是在所有中间件中使用 ctx.body 设置响应数据,但是并不立即响应,而是在所有中间件执行结束后,再调用 res.end(ctx.body) 进行响应,这样就为响应前的操作预留了空间,所以是请求与响应都在最外层,中间件处理是一层层进行,所以被理解成洋葱模型,个人拙见。

这个流程可以从源码 compse(middlewares) 后形成的函数执行处看到,这个合并的函数执行后有个 .then((ctx) => { res.end(ctx.body) }) 的操作,我们也可以通过在不同中间件中都设置 ctx.body,会发现响应数据被一次次覆盖。

核心就是请求的响应的时机不同,express是在调用res.send就结束响应了,而koa则是在中间件调用完成之后,在洋葱的最外层,由koa调用res.send方法。

以上便是koa与express中间件机制的不同了,写了很多,好辛苦,感觉有收获的话就鼓励下小编吧。

每天进步一点点,大家共勉。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-05-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 nodejs全栈开发 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档