欢迎关注我的公众号『 前端我废了 』,查看更多文章!!!
我们知道创建一个 Koa 应用主要分三步:
const Koa = require('koa');
// 1. 创建一个 Koa 实例
const app = new Koa();
// 2,加载多个中间件
app.use(/*中间件*/);
// ... 其他中间件
// 3. 指定服务器端口,创建一个 http 服务器
app.listen(3000);
那么中间件的执行顺序是怎样的呢?执行顺序是如何生成的呢?这篇文章就让我们来一探究竟。
如下示例代码,例如使用中间件 x-response-time
,logger
,response
const Koa = require('Koa');
const app = new Koa();
// 中间件1 x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// 中间件2 logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// 中间件3 response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
以上中间件的执行顺序,我们会常听到一个词叫做 “洋葱模型”(如下图),何为“洋葱模型“?洋葱内的每一层表示一个独立的中间件,用于实现不同的功能,比如日志记录,异常处理等。每次请求都会从左侧最外层开始,一层层经过中间件,当执行到最里层的中间件之后,接着从最里层的中间件开始逐层返回。因此对于每层的中间件来说,在一个 请求和响应 周期中,都有两个时机点来添加不同的处理逻辑。是不是有点像 DOM 事件流的事件捕获阶段(从外到里)和事件冒泡阶段(从里到外)。
上面中间件的执行顺序是怎么生成的呢?从 Koa 源码入手,从 Koa 源码的 `package.json 中 main 字段得知入口文件指向的是 lib/application.js 文件,详解一下创建一个 koa 应用执行过程:
const app = new Koa()
;我们 new Koa()
其实就是创建类 Application
的实例;app.use(/*中间件*/)
;当我们调用 app.use(function)
加载中间件时,use 函数内部将中间件函数都保存到了 middleware 数组里面;app.listen(/* 端口 */)
,内部使用 http.createServer()
创建一个 http 服务器,并调用 this.callback() 的返回值作为参数;callback 函数内部调用 compose 函数(即 koa-compose 中间件),就是造就中间件执行顺序的"幕后黑手"。下面我们逐行解析 koa-compose
源码,看看是如何生成 “洋葱模型”的。
koa-compose 源码(index.js 文件)内部导出的就是 compose 函数,我们除了看源码,也可以结合对应测试用例(test.js 文件)来理解。
/**
* compose 函数接收一个中间件数组,返回一个中间件函数
* @param {Array} middleware
* @return {Function}
*/
function compose (middleware) {
// 若传入的参数 middleware 不是数组,则抛错
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 若 middleware 数组项不是函数,则抛错
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* 返回一个中间件函数,第一参数为一个请求的上下文对象;第二个参数 next 函数为调用下一个中间件的函数
*/
return function (context, next) {
// 当前中间件在 middleware 数组中的索引位置
let index = -1
// 返回 dispatch(0) 结果
return dispatch(0)
// 该函数目的递归调用中间件,生成中间件嵌套调用结构
function dispatch (i) {
// 每个中间件函数内部只能调用一次 next 函数,否则抛错
// 测试用例 should throw if next() is called multiple times
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 记录索引
index = i
// 取出当前中间件函数
let fn = middleware[i]
// 若当前索引值等于中间件数组的长度,即 middleware 数组的中间件都处理完了,则 fn 赋值为参数 next
if (i === middleware.length) fn = next
// 若 fn 不存在,则 resolve 掉,结束
if (!fn) return Promise.resolve()
try {
// 返回 Promise,执行中间件函数 fn,这里将下一个中间件函数执行器作为第二个参数传入当前中间件,目的是将下一个中间件函数的执行权交由当前中间件,在其内部手动调用;
// 这里利用 bind 函数来实现,bind 函数执行后会返回一个新的函数,并不会立即执行,什么时候执行呢?也就是当前中间件 fn 函数里的 await next() 执行时,此时这个 next 函数也就是现在 fn 函数传入的第二个参数 dispatch.bind(null, (i + 1)的返回值
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
假如有三个中间件分别为 fn1
、fn2
、fn3
,那么在 Koa 内部经过中间件 koa-compose
组合后,将会生成如下的嵌套结构
// compose 函数返回值也是一个中间件函数
const fn = compose([fn1, fn2, fn3])
// compose 返回的函数 fn 内部嵌套结构,简略代码
function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};
以上就是生成中间件执行顺序的代码,核心就是 dispatch 函数,通过递归生成中间件嵌套调用结构,将下一个中间件的控制权通过函数参数的形式传递给当前中间件,以此类推,生成类似洋葱模型结构的执行顺序。
express 拥有路由、模板等框架常见功能,Koa 不含任何中间件,Koa 可被视为 node.js 的 http
模块的抽象,Express 则是 node.js 的应用程序框架。
中间件实现机制;express 基于 Callback,koa 基于 Promise
错误处理;express 对错捕获处理起来很不友好,每一个回调都拥有一个新的调用栈,因此你没法对一个 callback 做 try catch 捕获,你需要在 Callback 里做错误捕获,然后一层一层向外传递。
响应机制;express在调用 res.send 方法后就立即响应了,而koa则是在所有中间件调用完成之后,在最外层中间件进行响应。
引用一段其他网友总结的 xpress 和 koa 中间件机制的不同:
其实中间件执行逻辑没有什么特别的不同,都是依赖函数调用栈的执行顺序,抬杠一点讲都可以叫做洋葱模型。Koa 依靠 async/await(generator + co)让异步操作可以变成同步写法,更好理解。最关键的不是这些中间的执行顺序,而是响应的时机,Express 使用
res.end()
是立即返回,这样想要做出些响应前的操作变得比较麻烦;而 Koa 是在所有中间件中使用ctx.body
设置响应数据,但是并不立即响应,而是在所有中间件执行结束后,再调用res.end(ctx.body)
进行响应,这样就为响应前的操作预留了空间,所以是请求与响应都在最外层,中间件处理是一层层进行,所以被理解成洋葱模型,个人拙见。
牛逼!!!
如何更好地理解中间件和洋葱模型
多维度分析 Express、Koa 之间的区别
Koa与 express 的中间件机制揭秘
Egg.js 与 Koa
Koa-vs-express
Talk about koa’s onion model