中间件机制
面试高频考点:请手写 Koa 的中间件洋葱圈模型核心机制(
compose)
Koa 的中间件采用了洋葱圈模型。所有的请求在经过中间件的时候都会执行两次(“进”一次,“出”一次),这使得我们可以非常方便地执行后置处理逻辑。
例如,计算一个请求的响应时间:在中间件的开始记录初始时间,当 await next() 之后的代码执行时(说明内层所有的响应已处理完毕),再记录当前时间,即可计算出响应耗时。
function responseTime() {
return async function responseTime(ctx, next) {
const start = Date.now();
await next(); // 暂停当前中间件,等待下游中间件全部执行完毕
const delta = Date.now() - start;
ctx.set('X-Response-Time', delta + 'ms');
};
}
常用核心中间件
在 Koa 开发中,几乎所有的功能都依赖中间件:
koa-router:处理 URL 路由分发。koa-bodyparser:解析请求体(支持 json, form 等),将解析后的数据挂载到ctx.request.body。- 注意:它不支持解析
multipart/form-data(文件上传),通常需要用到@koa/multer或koa-body。
- 注意:它不支持解析
koa-cors:处理跨域资源共享(CORS)的请求头。koa-static:静态文件托管(如暴露 public 目录)。koa-session/koa-jwt:处理用户鉴权和状态保持。koa-compress:开启 Gzip / Brotli 压缩,减小传输体积。koa-logger:打印请求日志。
中间件底层原理 (koa-compose)
Koa 洋葱圈模型的核心在于 koa-compose 模块,它将多个中间件组合成一个大函数。
面试手写题:实现简易版的 compose 函数
核心思想:通过递归,利用闭包保存索引,把下一个中间件封装成 next 函数传给当前中间件。
function compose(middlewares) {
// 必须是数组,且每一项必须是函数
if (!Array.isArray(middlewares)) throw new TypeError('Middleware stack must be an array!');
for (const fn of middlewares) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!');
}
// 返回一个函数,该函数接收 context 和初始的 next
return function (context, next) {
// 记录上一次执行的中间件索引,防止在同一个中间件中多次调用 next()
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 = middlewares[i];
// 如果所有中间件执行完毕,将传入的 next 作为最后一个函数执行
if (i === middlewares.length) fn = next;
// 如果没有要执行的函数了,直接返回成功的 Promise
if (!fn) return Promise.resolve();
try {
// 执行当前的中间件,并将控制权交出
// 绑定的 next 参数就是 dispatch.bind(null, i + 1)
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
原理剖析
compose返回一个 Promise,这意味着即使最外层也可以通过.then来捕获整个链路的结果。- 通过
dispatch(i + 1),把下一个中间件包裹成了当前中间件的next参数。当我们在中间件里await next()时,其实就是等待dispatch(i + 1)这个 Promiseresolve。 - 如果一个中间件没有调用
await next(),后续的中间件将不再执行(比如权限校验失败,直接ctx.body = '403')。