Skip to main content

中间件机制

面试高频考点:请手写 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/multerkoa-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);
}
}
};
}

原理剖析

  1. compose 返回一个 Promise,这意味着即使最外层也可以通过 .then 来捕获整个链路的结果。
  2. 通过 dispatch(i + 1),把下一个中间件包裹成了当前中间件的 next 参数。当我们在中间件里 await next() 时,其实就是等待 dispatch(i + 1) 这个 Promise resolve
  3. 如果一个中间件没有调用 await next(),后续的中间件将不再执行(比如权限校验失败,直接 ctx.body = '403')。