Koa 洋葱模型

Koa 洋葱模型

提到 Node.js,就不得不提目前炙手可热的两大框架 Express 和 Koa,他俩都是 NodeJs 的主流开发框架。

Express 和 Koa:

Express是基于 Node.js 平台,快速、开放、极简的 Web 开发框架。
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

就现在的版本,koaexpress最大的区别也只有两点,这也是koa的最大的两个优点:

  1. koaexpress轻量的多,koa更像是一个中间件框架,只是一个基础的架子,需要用到的相应的功能时,用相应的中间件来实现就好,比如路由系统、静态资源托管等等。
  2. 对于 next() 的封装, koa 是返回 promise, express 是 void。所以koa的 next() 可以用 await 修饰,被 await 修饰的 next() 会等待上一个中间件执行完毕后再执行自身的 next()。
异步处理:

express是基于回调来处理,至于回调到底有多么的不好用?

// 原始使用回调版本
app.use((req, res, next) => {
  console.log('start');
  next();
  // 在这里,读取文件1,2的内容,并合并写入到3中
  fs.readFile('./file1.txt', (err1, data1) => {
    if (err) {
      console.log('failed in file1')
    }
    console.log(data1.toString());
    fs.readFile('./file2.txt', (err2, data) => {
      if (err2) {
        console.log('failed in file2')
      }
      console.log(data2.toString());
      fs.writeFile('./file3.txt', data1 + data2, err3 => {
        if (err3) {
          console.log('failed in file3')
        }
        console.log('done');
      });
    });
  });
});

Koa NoCallback! No Callback!No Callback!重要的事情说三遍!

Koa1 和 Koa2:

koa1基于co库,所以koa1利用Generator来代替回调,而koa2由于node7.6.0对async/await的支持,所以koa2利用的是async/await。
Koa1Koa2中间件的实现思路是一样的,只是实现方式有所区别。koa1 的中间件使用 generator 函数,使用 yield next 进入下一个中间件,koa2 中间件使用 async 函数,通过await next()进入下一个中间件。

中间件模型:

中间件

首先,中间件并不是 express/koa 独有的概念,所以广泛意义的中间件的意义是:
中间件是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的。
将上面的话翻译成人话就是:
在一次消费过程(在 koa 中可以看成是一次接受请求到响应结束的过程)中被调用的函数,就属于中间件。

中间件函数能够访问请求对象 (req)、响应对象(res) 以及应用程序的请求/响应循环中的下一个中间件函数,下一个中间件函数通常由名为 next 的变量来表示。
在这里插入图片描述
想象一下当业务逻辑复杂的时候,为了明确和便于维护,需要把处理的事情分一下,分配成几个部分来做,而每个部分就是一个中间件。

洋葱圈模型

Koa 的中间件模型就是洋葱圈模型。Koa的每个中间件就像是一个洋葱圈,每次当有一个请求进入的时候,每个中间件都会被执行两次。
在这里插入图片描述
在这里插入图片描述
每次执行下一个中间件传入两个参数 ctx 和 next,参数 ctx 是由 koa传入的封装了 request 和 response 的变量,可以通过它访问 request 和 response,next 就是下一个要进入执行的中间件。
其实很明显,在koa的中间件中,通过 next 函数,将中间件分成了两部分,next 上面的一部分会首先执行,而下面的一部分则会在所有后续的中间件调用之后执行。当一个中间件独调用 next() 则该函数暂停,并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件将恢复执行其上游行为。
当我们使用koa进行开发的时候,因为读取数据库或是http请求等都是异步请求,所以我们为了保证洋葱模型会使用号称异步终极解决方案的async/await。

const app = new Koa()
// 中间件A
app.use(async (ctx, next) => {
    console.log("A1")
    await next()
    console.log("A2")
});
// 中间件B
app.use(async (ctx, next) => {
    console.log("B1")
    await next()
    console.log("B2")
});
// 中间件C
app.use(async (ctx, next) => {
    console.log("C1")
    await next()
    console.log("C2")
});

app.listen(3000);
// 输出
// A1 -> B1 -> C1 -> C2 -> B2 -> A2

当程序运行到await next()的时候就会暂停当前中间件,进入下一个中间件,处理完之后才会再回过头来继续处理。也就是说,当一个请求进入,中间件1会被第一个和最后一个经过,中间件2则是被第二和倒数第二个经过,依次类推。
例如,我们经常需要计算一个请求的响应时间,在 Koa 中, 我们可以在中间件的开始记录初始时间,当响应返回时,代码执行又回到了原来的中间件,此时根据当前时间和初始时间的时间差便得到了响应时间。

function responseTime() {
  return async function responseTime(ctx, next) {
    const start = Date.now()
    await next() // wait for other middleware to run
    const delta = Math.ceil(Date.now() - start)
    ctx.set('X-Response-Time', delta + 'ms')
  })
}}
一个栗子

主要用来对比 koa 和 express 的执行顺序

let A = '';
app.use(async (ctx, next) => {
  A = await new Promise((resolve, reject) => {
    fs.readFile('./a.txt', function(err1, data1) {
      resolve(data1.toString());
    });
  });
  console.log('1');
  await next();
  console.log('1 call');
});

app.use(async (ctx, next) => {
  console.log('2');
  await next();
  console.log('2 call');
});

app.use(async (ctx, next) => {
  await new Promise((resolve, reject) => {
    fs.writeFile('./b.txt', A, function(err) {
      if(err) {
        reject(err);
      }
      resolve();
    });
  });
  console.log('3');
  next();
  const B = await new Promise((resolve, reject) => {
    fs.readFile('./b.txt', function(err, data) {
      if(err) {
        reject(err);
      }
      resolve(data.toString());
    });
  });
  console.log('3 call ',B);
});
// 1 => 2 => 3 => 3 call => 2 call => 1 call
// 1 => 2 => 2 call => 1 call => 3 => 3 call

洋葱圈核心实现

每个中间件都接收了一个next参数,在 next 函数运行之前的中间件代码会在一开始就执行,next函数之后的代码会在内部的中间件全部运行结束之后才执行。
要想达到上面洋葱圈的运行效果,我们需要做什么呢?

  1. 首先我们要知道当前中间件的数组集合
  2. 然后构建一个组合方法,对这些中间件按照洋葱的结构进行组合,并执行

我们带着这样的一个思路,再回头来看Koa是如何实现的:
3. this.middleware是中间件集合的数组
4. koa-compose模块的 compose 方法用来构建执行顺序

完美!下面只需要具体分析一下它们分别做了什么就可以了

// middleware用来保存中间件
app.use = (fn) => {
  this.middleware.push(fn)
  return this
}
// compose组合函数来规定执行次序
function compose (middleware) {
  // context:上下文,next:传入的接下来要运行的函数
  return function (context, next) {
    function dispatch (i) {
      // 中间件
      let fn = middleware[i]
      if (!fn) return Promise.resolve()
      try {
        // 我们这边假设和上文中的例子一样,有A、B、C三个中间件
        // 通过dispatch(0)发起了第一个中间件A的执行
        // A中间件执行之后,next作为dispatch(1)会被执行
        // 从而发起了下一个中间件B的执行,然后是中间件C被执行
        // 所有的中间件都执行了一遍后,执行Promise.resolve()
        // 最里面的中间件C的await next()运行结束,会继续执行console.log("C2")
        // 整个中间件C的运行结束又触发了Promise.resolve
        // 中间件B开始执行console.log("B2")
        // 同理,中间件A执行console.log("A2")
        return Promise.resolve(fn(context, () => {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
    return dispatch(0)
  }
}

Koa 利用了在中间件中间传入 next 参数的方法,再结合 middleware 中间件数组和 compose 组合函数,构建了洋葱圈的中间件执行结构。

完!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值