Koa2是基于Node.js平台的下一代web开发框架,它的核心特性之一就是中间件系统。中间件是处理HTTP请求的函数,能够访问请求对象、响应对象以及应用中的下一个中间件,通过组合不同的中间件可以实现日志记录、参数解析、权限校验等各种服务端功能。

Koa2中间件的基本使用
Koa2的中间件分为应用级中间件、路由级中间件等,最常用的是通过app.use()方法注册应用级中间件。下面是一个简单的中间件示例:
const Koa = require('koa');
const app = new Koa();
// 注册一个简单的中间件
app.use(async (ctx, next) => {
console.log('中间件开始执行');
await next(); // 调用下一个中间件
console.log('中间件执行结束');
ctx.body = 'Hello Koa2';
});
app.listen(3000, () => {
console.log('服务启动在3000端口');
});
上面的代码中,app.use()接收一个函数作为参数,这个函数就是中间件。函数接收两个参数,ctx是Koa的上下文对象,包含了请求和响应的所有信息,next是一个函数,调用它会执行下一个注册的中间件。
洋葱模型执行流程
Koa2中间件的执行遵循洋葱模型,也就是多个中间件会按照注册的顺序依次进入,执行完内部逻辑后再按照相反的顺序依次退出。假设我们注册三个中间件,执行顺序如下:
- 中间件1的前置逻辑执行
- 调用next(),进入中间件2的前置逻辑执行
- 调用next(),进入中间件3的前置逻辑执行
- 中间件3的后置逻辑执行
- 中间件2的后置逻辑执行
- 中间件1的后置逻辑执行
我们用代码验证这个流程:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('中间件1开始');
await next();
console.log('中间件1结束');
});
app.use(async (ctx, next) => {
console.log('中间件2开始');
await next();
console.log('中间件2结束');
});
app.use(async (ctx, next) => {
console.log('中间件3开始');
await next();
console.log('中间件3结束');
ctx.body = '中间件执行完毕';
});
app.listen(3000);
当我们访问服务时,控制台会输出:
中间件1开始
中间件2开始
中间件3开始
中间件3结束
中间件2结束
中间件1结束
这就是典型的洋葱模型执行效果,这种机制让开发者可以在中间件的前置部分做请求处理,后置部分做响应处理,非常灵活。
中间件原理核心实现
Koa2中间件的原理核心是中间件的收集和执行队列的实现。我们可以简化模拟Koa2的中间件实现逻辑:
中间件收集
Koa实例内部维护一个中间件数组,app.use()方法的作用就是把传入的中间件函数添加到这个数组中。
class SimpleKoa {
constructor() {
this.middlewares = []; // 存储所有中间件
}
use(fn) {
this.middlewares.push(fn); // 注册中间件到数组
}
}
中间件执行队列
Koa2通过koa-compose库来实现中间件的洋葱模型执行,核心逻辑是将所有中间件组合成一个函数,依次调用。我们简化实现这个组合函数:
// 组合中间件的 compose 函数
function compose(middlewares) {
return function(context) {
// 记录当前执行到第几个中间件
let index = -1;
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next函数不能调用多次'));
index = i;
let fn = middlewares[i];
if (!fn) return Promise.resolve(); // 所有中间件执行完毕
try {
// 调用中间件,传入上下文和下一个中间件的调用函数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0); // 从第一个中间件开始执行
};
}
完整模拟示例
我们把上面的逻辑组合起来,实现一个简化版的Koa中间件系统:
class SimpleKoa {
constructor() {
this.middlewares = [];
}
use(fn) {
this.middlewares.push(fn);
}
// 模拟 listen 方法,启动服务
listen(port, callback) {
const http = require('http');
const server = http.createServer((req, res) => {
const ctx = {
req,
res,
body: null
};
// 组合中间件并执行
const fn = compose(this.middlewares);
fn(ctx).then(() => {
res.end(ctx.body || '');
}).catch(err => {
res.end(err.message);
});
});
server.listen(port, callback);
}
}
// 测试简化后的Koa
const app = new SimpleKoa();
app.use(async (ctx, next) => {
console.log('第一个中间件开始');
await next();
console.log('第一个中间件结束');
});
app.use(async (ctx, next) => {
console.log('第二个中间件开始');
await next();
console.log('第二个中间件结束');
ctx.body = '简化后的Koa中间件执行成功';
});
app.listen(3000, () => {
console.log('服务启动在3000端口');
});
异步中间件支持
Koa2的中间件支持异步操作,因为compose函数中用了Promise.resolve包裹中间件的执行结果,所以无论是同步中间件还是async异步中间件,都能正常按照洋葱模型执行。比如我们可以在中间件中做异步操作:
app.use(async (ctx, next) => {
console.log('开始异步操作');
await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟1秒异步操作
await next();
console.log('异步操作结束');
});
这个中间件会先等待1秒,再执行下一个中间件,所有中间件执行完毕后,再执行自己的后置逻辑,完全符合洋葱模型的执行顺序。
常见注意事项
- 同一个中间件中,
next()函数不要调用多次,否则会触发错误,上面的compose函数已经做了重复调用的校验。 - 如果中间件中没有调用
next(),那么后续的中间件都不会执行,流程会直接返回。 - 中间件的执行顺序和
app.use()的注册顺序完全一致,先注册的先进入前置逻辑,后退出后置逻辑。 - 如果中间件中有异步操作,一定要用
await next(),否则异步操作没完成就会执行后续逻辑,导致执行顺序不符合预期。