express和koa都是基于nodejs的比较主流的两种web框架,express内置了很多中间件,而相对来说koa则更加轻量。
但对于异步处理,express用的是回调函数,koa1采用generator+yield,koa2采用异步终极解决方案async/await;通常我们说的koa就是指Koa2。
洋葱模型
洋葱模型的意思就是:每一层洋葱皮都相当于一个中间件,进入时穿过多少层,出来时就还得原路返回穿过这些层,koa中间件的执行顺序就符合这个洋葱模型。
ps.中间件就相当于处理请求的一个函数。通常用来对请求做一些通用的http请求的细节处理。
例如Koa中间件的执行顺序:
- 处理请求的顺序:第1层中间件-第2层-…-第n层-第n-1层-…-第2层-第1层
同步代码
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的next方法实现:
var idx = 0;//当前中间件索引
function next(err) {
...//此处只保留核心的代码,其余都省略
// find next matching layer
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
if (typeof match !== 'boolean') {
// hold on to layerError
layerError = layerError || match;
}
if (match !== true) {
continue;
}
...//省略路由部分
}
// no match
if (match !== true) {
return done(layerError);
}
...//省略
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
next(layerError || err)
} else if (route) {
layer.handle_request(req, res, next)
} else {
trim_prefix(layer, layerError, layerPath, path)
}
sync = 0
});
}
其实就是维护了一个存放中间件的栈stack,每次循环都会按顺序从栈中取出一个layer,这个layer包含了路由信息和中间件信息,然后调用matchLayer函数将这个layer和请求的路径做匹配,匹配成功就会执行layer.handle_request方法,递归调用中间件函数,如果失败就进入下一次循环。
Koa2:
constructor(options) {
this.middleware = [];
...//省略
}
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
use(fn) {
...//省略
this.middleware.push(fn);
return this;
}
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
if (!this.ctxStorage) {
return this.handleRequest(ctx, fn);
}
return this.ctxStorage.run(ctx, async() => {
return await this.handleRequest(ctx, fn);//注意这里的await
});
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
listen方法就是我们new出来的koa实例app后调用的app.listen方法,从以上代码可知该listen方法借用了this.callback方法来生成回调函数,作为创建http web服务器对象的参数。
use方法就是维护一个中间件的数组middleware,做一个push方法去新加中间件。
再看callback函数,可以看出这里创建了上下文,然后从this.handleRequest(ctx, fn)和const fn = compose(this.middleware)可以看出真正去调用中间件函数的方法是compose。
this.handleRequest会执行compose函数返回的Promise函数并返回结果。
function compose (middleware) {
...
return function (context, next) {
// last called middleware #
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]
// 当 fn 为空的时候,就会开始执行 next() 后面部分的代码
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
//执行下一个中间件 fn第一个参数是上下文,第二个是next函数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
i=0时,执行的是第一个中间件,之后也是按顺序一个接一个地调用,但当到最后一个中间件时,执行next的时候,进入dispatch函数时,fn会为undefined,这个时候就会进入到if(!fn)的判断条件中,执行promise.resolve。然后就会执行该中间件的next()语句之后的代码,然后反向回去。
参考链接:
https://zhuanlan.zhihu.com/p/417163957
https://juejin.cn/post/7044481311888654350