【Node】express vs koa 洋葱模型

131 阅读8分钟

【Node】express vs koa 洋葱模型

目录

[TOC]

洋葱模型是一种中间件流程控制方式。顾命就是用来控制流程的,我这里就讲解的很简单,主要把实现的思路做一个简单的讲解。

什么是洋葱模型

先看一张图:这张图在网上特别流行,基本搜索一下洋葱模型每篇文章都有这张图来做讲解。比较形象的解释了洋葱模型是在处理请求来和响应请求之间的问题。可以类比栈,先进后出。

在洋葱模型中,每一层相当于一个中间件,用来处理特定的功能,比如错误处理、 Session 处理等等。其处理顺序先是 next() 前请求( Request ,从外层到内层)然后执行 next() 函数,最后是 next() 后响应( Response ,从内层到外层),也就是说 每一个中间件都有两次处理时机

koa  vs  exprss

express和koa都是基于nodejs的比较主流的两种web框架,express内置了很多中间件,而相对来说koa则更加轻量。

但对于异步处理,express用的是回调函数,koa1采用generator+yield,koa2采用异步终极解决方案async/await;通常我们说的koa就是指Koa2。

同步代码

express:

const express = require("express")
const app = express()
 
app.use((req, res, next) => {
  console.log("第一层中间件start")
  next()
  console.log("第一层中间件end")
  res.send("hello world")
})
 
app.use((req, res, next) => {
  console.log("第二层中间件start")
  next();
  console.log("第二层中间件end")
})
 
app.use((req, res, next) => {
  console.log("第三层中间件start");
  next()
  console.log("第三层中间件end");
})
 
app.listen(3000)

输出结果:

第一层中间件start
第二层中间件start
第三层中间件start
第三层中间件end
第二层中间件end
第一层中间件end

Koa:

const Koa = require("koa")
const app = new Koa()
 
app.use((ctx, next) => {
  console.log("第一层中间件start")
  next()
  console.log("第一层中间件end")
  ctx.body = "hello world"
})
 
app.use((ctx, next) => {
  console.log("第二层中间件start")
  next()
  console.log("第二层中间件end")
})
 
app.use((ctx, next) => {
  console.log("第三层中间件start")
  next()
  console.log("第三层中间件end")
})
 
app.listen(3000)

输出结果:

第一层中间件start
第二层中间件start
第三层中间件start
第三层中间件end
第二层中间件end
第一层中间件end

可以看出两个框架在同步代码的情况下,得到的结果是一致的,都符合洋葱模型的执行顺序。

异步代码

Express:

const express = require("express")
const app = express()
 
app.use((req, res, next) => {
  console.log("第一层中间件start")
  next()
  console.log("第一层中间件end")
  res.send("hello world")
})
 
app.use((req, res, next) => {
  console.log("第二层中间件start")
  next();
  console.log("第二层中间件end")
})
 
app.use(async(req, res, next) => {
  console.log("第三层中间件start");
  await new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log("异步");
      resolve()
    }, 1000);
  })
  console.log("第三层中间件end");
})
 
app.listen(3000)

输出结果:

第一层中间件start
第二层中间件start
第三层中间件start
第二层中间件end
第一层中间件end
异步
第三层中间件end

Koa:

const Koa = require("koa")
const app = new Koa()
 
app.use(async (ctx, next) => {
  console.log("第一层中间件start")
  await next()
  console.log("第一层中间件end")
  ctx.body = "hello world"
})
 
app.use(async(ctx, next) => {
  console.log("第二层中间件start")
  await next()
  console.log("第二层中间件end")
})
 
app.use(async(ctx, next) => {
  console.log("第三层中间件start")
  await new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log("异步");
      resolve()
    }, 1000);
  })
  console.log("第三层中间件end")
})
 
app.listen(3000)

输出结果:

第一层中间件start
第二层中间件start
第三层中间件start
异步
第三层中间件end
第二层中间件end
第一层中间件end

可以看到koa对于异步代码仍然是严格遵循洋葱模型,但是express则没有。

koa的中间件模式是洋葱模型,而express的中间件模式是直线型,这种区别的核心所在就是因为它们的中间件执行机制不同,next函数的实现原理不一样,koa的next()会返回一个promise实例,而express的next()返回void;express递归回调中不会去等待中间件中的异步函数执行完毕,而koa则存在await中间件异步函数。

express的洋葱模型

大家只要讲洋葱模型,就会联想到 koa 的中间件,很少有人谈及 express 的洋葱模型和中间件原理。那么我就来反其道而行之,讲讲 express 的洋葱模型。以下是栗子:

const express = require('express')
const app = express();
 
const A = function A(req,res,next) {
	console.log('A 开始')
	next()
	console.log('A 结束')
}
const B = function B(req,res,next) {
	console.log('B 开始')
	next()
	console.log('B 结束')
}
const C = function C(req,res,next) {
	console.log('C 开始')
	next()
	console.log('C 结束')
}
 
app.get('/',A,B,C)
 
A 开始
B 开始
C 开始
C 结束
B 结束
A 结束

以上代码可以直接复制,直接运行,运行结果就是 A=>B=>C=B=>A 的结构,正如洋葱模型的结构。以上代码其实等价于以下代码,你可以理解为3个函数的调用是嵌套的, A 点用 BB 调用 CC 结束释放, B 结束释放, A 结束释放的这么的一个流程。

function A() {
	console.log('A 开始')
	function B() {
		console.log('B 开始')
		function C() {
			console.log('C 开始')
			console.log('C 结束')
		}
		C()
		console.log('B 结束')
	}
	B()
	console.log('A 结束')
}
如何实现

大概的一个思想和逻辑我们已经了解,那该怎么将一个顺序执行的变成内部调用的逻辑呢?

let index = -1
const FnArr = [A,B,C]
function next() {
	index++
	FnArr[index](next)
	if(index >= FnArr.length) { 
		return 
	}
}
next()

实现的逻辑非常简单,每次迭代都是将next传递给用户,用户手动调用next之后就会跳到下个函数的执行,以此来达到内部迭代的目的

express内部实现

espressnext 就是用来迭代中间件的, handle_request 可以认为就用传进来的方法也就是 A,B,C 每次迭代都会把当前的 next 传给用户,用户手动调用的时候,就会再次触动这个 next 函数,利用了闭包保留了 idxstack

next();
 
  function next(err) {
    // signal to exit route
    if (err && err === 'route') {
      return done();
    }
 
    // signal to exit router
    if (err && err === 'router') {
      return done(err)
    }
 
    var layer = stack[idx++];
    if (!layer) {
      return done(err);
    }
 
    if (layer.method && layer.method !== method) {
      return next(err);
    }
 
    if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }

espressnext 就是用来迭代中间件的, handle_request 可以认为就用传进来的方法也就是 A,B,C 每次迭代都会把当前的 next 传给用户,用户手动调用的时候,就会再次触动这个 next 函数,利用了闭包保留了 idxstack

Koa 使用洋葱模型

假如不是洋葱模型,我们中间件依赖于其他中间件的逻辑的话,我们要怎么处理?

比如,我们需要知道一个请求或者操作 db 的耗时是多少,而且想获取其他中间件的信息。在 koa 中,我们可以使用 async await 的方式结合洋葱模型做到。

 
app.use(async (ctx, next) => {
  const start = new Date();
  await next();
  const delta = new Date() - start;
  console.log(`请求耗时: ${delta} MS`);
  console.log("拿到上一次请求的结果:", ctx.state.baiduHTML);
});
 
app.use((ctx, next) => {
  // 使用定时器模拟异步请求
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      ctx.state.baiduHTML = "模拟的请求数据";
      console.log("异步请求完成");
      resolve();
    }, 2000);
  });
});

而假如没有洋葱模型,这是做不到的。


如何实现

首先,先看看middleware在源码里是什么数据类型:

然后按流程看,肯定先进app的listen函数:

创建服务的时候,传入了callback函数的返回值,去看看callback函数:

重点就是这里了,我们上面的分析说明想要实现洋葱模型,下面两点缺一不可:

  • 要把上下文ctx对象和下一个中间件next传给当前的中间件
  • 必须要等待下一个中间件执行完,再执行当前中间件的后续逻辑

而这就是compose函数所做的事情,来自于 koa-compose ,这里先暂时不贴源码,有一说一很绕,强行看有点难受

所以,我们可以先按自己的思路来试试:

应该不需要解释吧,这样肯定会报错:

因为执行mw2的时候(也就是mw1里的next),并没有把ctx 和 mw3传给它

那么问题来了:我们怎么才能在调用mw1的next时,把ctx 和 mw2给这个next呢?

那我们肯定就需要对middleware数组里的每个元素重新包装一下了,用什么包装呢?

看个例子:

bind 会将当时的参数保留下来,这正是我们所需要的,因此,加上一点小小的改动:

这个时候我们再跑一下代码:

这不就实现了吗?刚刚我留了一个坑就是没放 koa-compose 的源码,下面是源码:

红框的部分就是核心代码,大家可以自己看看,如果感觉很绕,可以对比我上面的例子先理解的,

这个时候就会执行第三个中间件 next() 之后的代码,然后是第二个、第一个,从而形成了洋葱模型。

其过程如下所示:

简易版 compose

模范 koa 的逻辑,我们可以写一个简易版的 compose 。方便大家的理解:

const middleware = []
let mw1 = async function (ctx, next) {
    console.log("next前,第一个中间件")
    await next()
    console.log("next后,第一个中间件")
}
let mw2 = async function (ctx, next) {
    console.log("next前,第二个中间件")
    await next()
    console.log("next后,第二个中间件")
}
let mw3 = async function (ctx, next) {
    console.log("第三个中间件,没有next了")
}
 
function use(mw) {
  middleware.push(mw);
}
 
function compose(middleware) {
  return (ctx, next) => {
    return dispatch(0);
    function dispatch(i) {
      const fn = middleware[i];
      if (!fn) return;
      return fn(ctx, dispatch.bind(null, i+1));
    }
  }
}
 
use(mw1);
use(mw2);
use(mw3);
 
const fn = compose(middleware);
 
fn();