【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点用B,B调用C,C结束释放,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内部实现
espress中next就是用来迭代中间件的,handle_request可以认为就用传进来的方法也就是A,B,C每次迭代都会把当前的next传给用户,用户手动调用的时候,就会再次触动这个next函数,利用了闭包保留了idx和stack。
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);
}
}
espress 中 next 就是用来迭代中间件的, handle_request 可以认为就用传进来的方法也就是 A,B,C 每次迭代都会把当前的 next 传给用户,用户手动调用的时候,就会再次触动这个 next 函数,利用了闭包保留了 idx 和 stack 。
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();