koa入坑及其中间件原理

Posted cxz520

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了koa入坑及其中间件原理相关的知识,希望对你有一定的参考价值。

技术图片

 

看见了吧,刚入坑koa总是会看见一张洋葱图(后面再说吧)

刚入坑一个东西的时候,我们总会问这个东西是什么?这个东西是用来做什么的?这个东西包括了哪些内容以及它的原理?

 

那好,我们先来解决第一个问题,koa.js是什么东西?

koa.js是一个框架。

 

哈哈哈哈,第二个问题,koa.js用来做什么的?

① 对于 HTTP 服务

  当前端 UI 向服务器发起了一个 HTTP 请求时,koa.js 能够在 HTTP 请求发送后 (要搞清楚,是已经发送出去的请求,并不是像 axios 一样拦截 request )对于该请求的 request 进行处理

  既然能对请求的 request 进行处理,那么 koa.js 也能对于服务端返回的 HTTP 响应(跟上述一样,是已经发出的 response )进行处理。

② 对于中间件容器

  对于这个中间件容器,我也没什么概念,于是乎... 我去找了下相关的内容。

  什么是中间件容器呢?

  在一个大型分布式的系统中, 负责各个不同组件和服务之间的管理和交互。(哎,简单来说点就是你,你爸,你妈,你想要找你爸要点零花钱,但是必须通过你妈哈哈哈哈哈)

  比如呢,在一个分布式系统中,有N个数据库,数据库按业务模块分配,但是,随时间增加,业务会改变,而对应的数据库的分配也需要改变。这时候,你如果想要修改某些数据,直接操作数据库是不太可能了,那这时候就需要一个中间件,由它来负责对最终的数据库读写,而访问者只是带着一些参数啥的来访问这个中间件。

  言归正传... 切回到主题 koa.js 对于中间件容器能干嘛呢?

  第一点:中间的加载

  第二点:中间的执行

 

 

 

 对于 koa.js的作用也就这两点了。

 

讲完了它是啥,做什么的,该来讲讲它的内容了。

在使用 Koa.js 的过程中,会发现中间件的使用都是如下所示:

 

const Koa = require(‘koa‘);
let app = new Koa();

const middleware1 = async (ctx, next) => { 
  console.log(1); 
  await next();  
  console.log(6);   
}

const middleware2 = async (ctx, next) => { 
  console.log(2); 
  await next();  
  console.log(5);   
}

const middleware3 = async (ctx, next) => { 
  console.log(3); 
  await next();  
  console.log(4);   
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async(ctx, next) => {
  ctx.body = ‘hello world‘
})

app.listen(3001)

// 启动访问浏览器
// 控制台会出现以下结果
// 1
// 2
// 3
// 4
// 5
// 6

 

问题来了,为什么会出现以上的结果?

这个主要是 Koa.js 的一个中间件引擎 koa-compose 模块来实现的,也就是 Koa.js 实现洋葱模型的核心~~

从洋葱模型可以看出来(怎么看?就是从左到右穿过去.. 先碰到最外圈.. 最后也是碰到的最外圈),中间件在 await next() 前后的操作,很像数据结构-----“栈”,先进后出,符合洋葱模型。同时,在上面的代码中还有 ctx,这个是就是上下文管理操作数据了(哎,写详细点吧,这个上下文就是来保持两个函数之间传递的时候的一些参数状态吧)。于是乎我们可以总结出如果要实现洋葱模型需要具有的几点特性。

① 需要有统一的 上下文 ctx

② 操作先进后出

③ 有控制先进后出的机制 next 

④ 有提前结束的机制

知道了这几个特性,可以自己单纯的用 Promise 做个实现。

// 定义一个上下文
let context = {
    data: []
}; 

// 定义一个中间件 middleware1
async function middleware1( ctx, next ){
    console.log(‘action 001‘);
    ctx.data.push(1);
    await next();
    console.log(‘action 006‘);
    ctx.data.push(6);
}

// 定义一个中间件 middleware2
async function middleware2(ctx, next) {
  console.log(‘action 002‘);
  ctx.data.push(2);
  await next();
  console.log(‘action 005‘);
  ctx.data.push(5);
}

// 定义一个中间件 middleware3
async function middleware3(ctx, next) {
  console.log(‘action 003‘);
  ctx.data.push(3);
  await next();
  console.log(‘action 004‘);
  ctx.data.push(4);
}

Promise.resolve(middleware1(context, async() => {
  return Promise.resolve(middleware2(context, async() => {
    return Promise.resolve(middleware3(context, async() => {
      return Promise.resolve();
    }));
  }));
})).then(() => {
    console.log(‘end‘);
    console.log(‘context = ‘, context);
  });

// 结果显示
// "action 001"
// "action 002"
// "action 003"
// "action 004"
// "action 005"
// "action 006"
// "end"
// "context = { data: [1, 2, 3, 4, 5, 6]}"

然而,虽然单纯用 Promise 嵌套 可以直接实现中间件流程,但是这样会产生代码可读性和可维护性的问题,也带来了中间件扩展的问题。所以,这需要把 Promise 嵌套 实现的中间件方式进行高度抽象,达到可以自定义中间件的层数。这时候就需要借助神器 async/await 

首先理清一下需要的步骤:

  • 中间件队列
  • 处理中间队列,并将上下文传进去
  • 中间件的流程控制器 next
  • 异常的处理

根据上面中间件分析的原理,可以抽象出

  • 每一个中间件需要封装一个 Promise
  • 洋葱模型的先进后出操作应该要对应 Promise.resolve 的前后操作
const compose = require(‘./index‘);
// 中间件数组
let middleware = [];
// 上下文
let context = {
  data: []
};
// 添加第一个中间件
middleware.push(async(ctx, next) => {
  console.log(‘action 001‘);
  ctx.data.push(2);
  await next();
  console.log(‘action 006‘);
  ctx.data.push(5);
});
// 添加第二个中间件
middleware.push(async(ctx, next) => {
  console.log(‘action 002‘);
  ctx.data.push(2);
  await next();
  console.log(‘action 005‘);
  ctx.data.push(5);
});
// 添加第三个中间件
middleware.push(async(ctx, next) => {
  console.log(‘action 003‘);
  ctx.data.push(2);
  await next();
  console.log(‘action 004‘);
  ctx.data.push(5);
});

// 得到一个函数,相当于是一个中间件的链吧
const fn = compose(middleware);

// 执行fn函数,传入上下文
fn(context)
  .then(() => {
    console.log(‘end‘);
    console.log(‘context = ‘, context);
  });

以下是compose

module.exports = compose;

// 放入中间件
function compose(middleware) {
  //判断是不是数组
  if (!Array.isArray(middleware)) {
    throw new TypeError(‘Middleware stack must be an array!‘);
  }

  // 返回一个方法
  return function(ctx, next) {
    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();
      }
      
      // 这个就是拼装一个 Promise 的链子吧
      try {
        return Promise.resolve(fn(ctx, () => {
          return dispatch(i + 1);
        }));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

至此原理已经看透透,其实就是一个 诺言 的链子~ 来进行的中间件调用~

还未讲完~~~ 后文待续~~~~

ε=(´ο`*)))唉

 

借鉴原文 https://chenshenhai.github.io/koajs-design-note/note/chapter01/05.html

以上是关于koa入坑及其中间件原理的主要内容,如果未能解决你的问题,请参考以下文章

koa2中间件koa和koa-compose源码分析原理

玩转Koa -- 核心原理分析

深入解析Koa之核心原理

KOA中间件实现原理

理解 Koa 框架中间件原理

深入理解 Koa 框架中间件原理