Koa2 与 Express 对比
面试高频考点:Koa2 和 Express 的区别
- 中间件模型
- Koa2 是基于
async/await的洋葱圈模型(中间件可以控制请求和响应两次过程)。 - Express 是线性的栈执行模型,通过回调
next()继续,一般只控制进入过程。
- Koa2 是基于
- 响应时机
- Express 中直接调用
res.send()或res.json()就会立即发送响应,后续的中间件即使执行也无法修改响应体。 - Koa2 是将数据挂载在
ctx.body上,它需要等待所有的中间件执行完毕后,由最外层的处理函数统一进行响应发送。
- Express 中直接调用
- 异步方式
- Express 采用 Callback(回调函数),虽然支持 Promise,但在处理异常时容易导致“回调地狱”,且容易漏捕获错误。
- Koa2 完全拥抱
async/await,代码结构更加扁平化。
- 异常处理
- Express 中,通常使用 error-first 的回调模式或全局错误处理中间件。
- Koa2 中,最外层加一个
try-catch中间件,利用await next(),可以非常优雅地捕获下游所有中间件和路由的异常。
- 设计哲学
- Express 内置了路由(Router)、视图渲染等诸多功能,是个大而全的框架。
- Koa2 是一个极其轻量级的内核,本身甚至不带路由,所有的功能(如路由、静态服务)都需要依靠引入第三方中间件实现,更加纯粹。
中间件与洋葱圈模型
示例代码如下:
const Koa = require("koa");
const app = new Koa();
// x-response-time 中间件
app.use(async (ctx, next) => {
console.log("\n开始 x-response-time");
const start = Date.now();
await next(); // 调用下一个中间件:logger(等待下一个异步函数返回)
const ms = Date.now() - start;
ctx.set("X-Response-Time", `${ms}ms`); // 设置响应头
console.log("结束 x-response-time\n");
});
// logger 中间件
app.use(async (ctx, next) => {
console.log("开始 logger");
const start = Date.now();
await next(); // 调用下一个中间件:response(等待下一个异步函数返回)
const ms = Date.now() - start;
console.log(
`\u0020\u0020\u0020\u0020\u0020${ctx.method} ${ctx.url} - ${ms} ms`,
); // \u0020 为空格
console.log("结束 logger");
});
// response 中间件
app.use(async (ctx) => {
console.log("开始 response");
ctx.body = "Hello World";
console.log("结束 response");
// 没有更多的中间件执行,堆栈将展开并且每个中间件恢复执行其上游行为
});
app.listen(3000);
console.log("app started at port 3000...");
如果一个中间件 没有调用 await next(),后续的 middleware 将不再执行了。例如,一个检测用户权限的 middleware 可以决定是否继续处理请求,还是直接返回 403 错误
app.use(async (ctx, next) => {
if (await checkUserPermission(ctx)) {
await next();
} else {
ctx.response.status = 403;
}
});
原理剖析
中间件底层原理 (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')。
常见中间件实现
koa-cors
const URL = require("url");
module.exports = async function (ctx, next) {
let origin = URL.parse(ctx.origin) || "";
if (origin.protocol && origin.host) {
ctx.set(
"Access-Control-Allow-Origin",
`${origin.protocol}://${origin.host}`,
);
ctx.set(
"Access-Control-Allow-Headers",
"X-Requested-With, User-Agent, Referer, Content-Type, Cache-Control, accesstoken",
);
ctx.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
ctx.set("Access-Control-Max-Age", "86400");
ctx.set("Access-Control-Allow-Credentials", "true");
}
if (ctx.methods === "OPTIONS") {
ctx.status = 204;
ctx.body = "";
} else {
await next();
}
};
常用核心中间件
在 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:打印请求日志。