举一反三,手撕源码,探究Koa洋葱与中间件的实现!

3,141 阅读6分钟

大家好,我是半夏👴,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注➕ 点赞 👍 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师~关注公众号:搞前端的半夏,了解更多前端知识! 点我探索新世界!

前言

在上一篇大前端,你说你不会Koa?进来带你手撸源码,看完不会,尽管喷!,介绍了Koa上下文的实现,以及Koa对node原生http模块request和response的封装。

为了新读者可以直接阅读这篇文章,我们再来梳理一下。

导出Koa类,以及listen,use函数的实现。

let http = require('http')
class Application {
    use(fn) {
        this.fn=fn

    }
    callback = (req, res) => {
        this.fn(req, res)
    }
  
    listen() {
        const server = http.createServer(this.callback);
        server.listen(...arguments)
    }
}
module.exports = Application

封装ctx,我们新增了一个函数,在callback中调用。让然后传给use的函数。

 createContext = (req, res) => {
 // 这里主要是将原生的req和res绑定到ctx上。
  }
  callback = (req, res) => {
      let ctx = this.createContext(req, res)
      this.fn(ctx)
  x

响应用户请求,返回内容

callback = (req, res) => {
  let ctx = this.createContext(req, res)
  this.fn(ctx)
  let body = ctx.body;
  if (body) {
      res.end(body);
  } else {
      res.end('Not Found')
  }
}

这样简单一看,Koa还是很简单的。当然不是,这只是最基本的源码,本文,我们就要来手撕Koa的中间件系统的实现。

洋葱模型

这个概念基本是每一篇关于Koa的文章都会介绍的。并且都会放下面这张图。

这玩意是就是洋葱模型,不看代码的话,我们最简单的理解是啥呢:

koa通过app.use加载外部的函数,在创建完上下文之后,按照上面我们正常的操作,我们是直接响应用户请求,返回内容,但是洋葱模型干了啥?我们此时并不会直接去响应,而是先把加载的外部函数执行完,再去响应用户请求,返回内容。

callback = (req, res) => {
  let ctx = this.createContext(req, res)
  use1的函数执行
  use2的函数执行
  use3的函数执行
  let body = ctx.body;
  if (body) {
      res.end(body);
  } else {
      res.end('Not Found')
  }
}

上面的代码只是我们最简单的状态,不存在异步的情况。

app.use与中间件

Koa中间件其实就是函数,通过app.use来进行调用。app.use() 返回 this, 因此可以链式表达.

这里的中间件可以时普通的函数,也可以是async定义的异步函数。

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

这里的next是为了去执行下一个中间件,意思就是下面的console.log(1)不执行,执行别的中间件去。

再聊聊洋葱模型。上面我们简单的猜测了一下洋葱模型的逻辑。下面我们通过一个例子再来说明一下。

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

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

app.use(async (ctx, next) => {
    console.log(3)

})

对于这个例子输出的结果是 1 2 3 2 1。 再来看下这幅图,是不是知道洋葱模型的大概了。

上面我们说了next的作用是去执行下一个中间件,在上面的例子我们做一个改动。

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

app.use(async (ctx, next) => {
    console.log(3)

})

app.use((ctx) => {
    console.log('koa')
    ctx.body = "原生koad"
})
app.use(async (ctx, next) => {
    console.log(4)
    next();
    console.log(4)
})

对于这个例子,最后两个中间件是无法执行到的。

话不多说;我们总结一下洋葱模型: 中间的执行,并不是一层一层的执行,而是以next为界限,先执行next上面的代码,知道所有的上层执行完,再执行next下层代码。

一个洋葱结构,从上往下进来,再从下往上回去。

步步升级

最简单

我们上面已经知道了洋葱模型的大概实现,那么我们就先简单的实现一下。 首先是next的作用,这里next的作用就像是一个占位符,或者说是执行器,遇到next就会去执行下一个函数,我们来简单的模拟一下:

function fn1() {
  console.log(1)
  fn2();
  console.log(1)
}
function fn2() {
  console.log(2)
  fn3();
  console.log(2)
}

function fn3() {
  console.log(3)
  return;
}
fn1();

我们将函数放在next的位置,就可以达到效果。

升级-函数包裹

上面我们把函数放在了next的位置,是可以实现效果。 那么我们现在把next放回去。

async function fn1(next) {
  console.log(1);
  await next();
  console.log(1);
}

async function fn2(next) {
  console.log(2);
  await next();
  console.log(2);
}

async function fn3() {
  console.log(3);
}

let next1 = async function () {
  await fn2(next2);
}
let next2 = async function() {
  await fn3();
}
fn1(next1);

这里我们将函数包裹起来赋值给next。然后传给函数。再调用。

升级-封装共通

上面的例子我们将函数包裹起来,其实也就是将函数传给另一个函数,如果我们有N个函数,如果你不嫌麻烦也可以这样写,但是我们可以根据上面的思路提出一个共通的函数。

首先:

  1. 中间件是普通函数也可以是async函数。
  2. 中间件接受next来调用下一个中间件
  3. 先执行第一个中间件

在上面我们进行next传参的时候,创建了next1和next2二次封装中间价,最后执行第一个函数。现在我们想要使用一个next来进行封装,第一反应肯定是在for或者while中,这里怎么会出现for,既然我们有这么多中间件,把他们收集起来,然后循环对他们传参,是不是就可以了。

收集函数

const middlewares = [fn1, fn2, fn3];

传参next

这个函数的作用就是给每一个中间价传参。

function compose(middleware, next) {
  return async function() {
    await middleware(next);
  }
}

循环传参

这里定义了一个next,来保存上一个已经接受next的函数。

let next;
for (let i = middlewares.length - 1; i >= 0; i--) {
   next = compose(middlewares[i], next);

}

经过这个for循环,我们已经完整的给每个中间件传递了next。效果大概就是这种。

next = async function(){
  await fn1(async function() {
    await fn2(async function() {
      await fn3(async function(){
        return Promise.resolve();
      });
    });
  });
};

async function fn1(next) {
  console.log(1);
  await fn2();
  console.log(1);
}

async function fn2(next) {
  console.log(2);
  await fn3();
  console.log(2);
}

async function fn3() {
  console.log(3);
}

这里有一个点,我们for是从最后一个开始的,这是为了,能够执行函数,我们传递结束参数,那么肯定要执行函数,next最后保存的就是第一个函数。

完整代码

async function fn1(next) {
  console.log(1);
  await fn2();
  console.log(1);
}

async function fn2(next) {
  console.log(2);
  await fn3();
  console.log(2);
}

async function fn3() {
  console.log(3);
}

function compose(middleware, oldNext) {
  return async function() {
    await middleware(oldNext);
  }
}

const middlewares = [fn1, fn2, fn3];

let next ;

for (let i = middlewares.length - 1; i >= 0; i--) {
  next = compose(middlewares[i], next);
}
next();

Koa的实现

上面简单的实现了一个洋葱模型。 在Koa中实现的逻辑其实大致相同。

this.middlewares

constructor() {
  this.middlewares = [];
}

之前我们use是

use(fn) {
      this.fn=fn
  }

现在只要:

use(fn) {
  this.middlewares.push(fn)
}

将中间件收集起来

compose

之前我们在callback中执行了

callback = (req, res) => {
  this.fn(ctx)
}

现在有了中间件的概念我们肯定就不能再这样写了。 同样的,我们将所有的中间件封装到一个函数中,然后执行。

callback = (req, res) => {
    this.compose(ctx).then(() => {
            let body = ctx.body;
            if (body) {
                res.end(body);
            } else {
                res.end('Not Found')
            }
        }).catch((e)=>{
            
        })
}
  

compose内部就是上面的for循环/while循环。在Koa中使用的是while循环。这里我们也用while循环实现一遍。

  1. while循环时从头往后,同时一个中间件中只有一个next。
  2. 当走到最后一个中间件,返回一个promsie
  3. 最后执行第一个中间件
  compose(ctx){
        // index
        let index = -1
        const dispatch = (i)=>{
            if(i <= index){
                return Promise.reject('next() 只允许调用一次')
            }
            index = i;
            if(this.middlewares.length == i) return Promise.resolve();
            let middleware = this.middlewares[i];
            try{
                return Promise.resolve(middleware(ctx,()=> dispatch(i+1))); 
            }catch(e){
                return Promise.reject(e)
            }
        }
        return dispatch(0); 
    }

至此整个洋葱模型的实现结束。