后端
2022年8月1日
17 分钟阅读
Koa.js 洋葱模型深度解析:中间件执行机制
Koa.js 洋葱模型深度解析:中间件执行机制
Koa.js 是 Express 原班人马打造的新一代 Node.js Web 框架,其核心特性之一就是基于洋葱模型(Onion Model)的中间件执行机制。这种设计让中间件的执行顺序更加清晰、可控,也使得异步流程处理更加优雅。本文将从原理到实践,深入解析 Koa 洋葱模型的工作机制。
什么是洋葱模型?
基本概念
洋葱模型是一种中间件执行模式,其执行流程类似于剥洋葱:
- 请求阶段(入栈):请求从外层中间件开始执行,逐层向内传递
- 响应阶段(出栈):响应从内层中间件开始返回,逐层向外传递
这种双向执行模式让每个中间件都有机会在请求前后进行处理。
执行流程图
请求 ──> 中间件1 ──> 中间件2 ──> 中间件3 ──> 路由处理
│ │ │ │ │
│ before 1 before 2 before 3 handler
│ │ │ │ │
│ │ next() 1 next() 2 next() 3
│ │ │ │ │
响应 <── 中间件1 <── 中间件2 <── 中间件3 <── 响应数据
│ after 1 after 2 after 3
Koa 中间件基础
中间件签名
Koa 中间件是一个异步函数,接收两个参数:
async function middleware(ctx, next) {
// ctx: Koa 上下文对象,包含 request、response 等
// next: 下一个中间件的调用函数
// 请求阶段处理
console.log("Before next()");
await next(); // 调用下一个中间件
// 响应阶段处理
console.log("After next()");
}简单示例
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx, next) => {
console.log("1. Before next()");
await next();
console.log("1. After next()");
});
app.use(async (ctx, next) => {
console.log("2. Before next()");
await next();
console.log("2. After next()");
});
app.use(async (ctx) => {
console.log("3. Handler");
ctx.body = "Hello Koa";
});
app.listen(3000);执行顺序:
1. Before next()
2. Before next()
3. Handler
2. After next()
1. After next()
next() 机制深入理解
next() 的本质
next() 实际上是一个返回 Promise 的函数,它负责:
- 暂停当前中间件的执行
- 调用下一个中间件
- 等待下一个中间件执行完成
- 返回执行权给当前中间件
// 简化版的 next() 实现原理
function createNext(middlewares, index) {
return async function next() {
if (index >= middlewares.length) {
return; // 所有中间件执行完毕
}
const middleware = middlewares[index];
// 递归调用下一个中间件
await middleware(ctx, createNext(middlewares, index + 1));
};
}同步与异步执行
app.use(async (ctx, next) => {
console.log("1. Start");
// ❌ 错误:没有 await,无法等待下一个中间件完成
next(); // 不会等待
console.log("1. End"); // 会立即执行,不等待中间件 2
});
// ✅ 正确:使用 await
app.use(async (ctx, next) => {
console.log("1. Start");
await next(); // 等待下一个中间件完成
console.log("1. End"); // 在所有后续中间件执行完后才执行
});错误处理中的 next()
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
// 捕获后续中间件抛出的错误
ctx.status = err.statusCode || err.status || 500;
ctx.body = {
message: err.message,
};
}
});
app.use(async (ctx, next) => {
if (!ctx.query.token) {
throw new Error("Token required"); // 错误会被上层捕获
}
await next();
});洋葱模型的实际应用
请求日志中间件
// logger.js
async function logger(ctx, next) {
const start = Date.now();
// 请求阶段:记录请求信息
console.log(`${ctx.method} ${ctx.url}`);
await next();
// 响应阶段:记录响应信息
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms - ${ctx.status}`);
}
app.use(logger);响应时间中间件
async function responseTime(ctx, next) {
const start = Date.now();
await next();
const ms = Date.now() - start;
// 在响应头中添加响应时间
ctx.set("X-Response-Time", `${ms}ms`);
}
app.use(responseTime);错误处理中间件
// 错误处理中间件应该放在最外层
async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
// 统一错误处理
ctx.status = err.statusCode || err.status || 500;
ctx.body = {
error: {
message: err.message,
code: err.code,
...(process.env.NODE_ENV === "development" && { stack: err.stack }),
},
};
// 记录错误日志
ctx.app.emit("error", err, ctx);
}
}
app.use(errorHandler);认证中间件
async function auth(ctx, next) {
const token = ctx.headers.authorization?.replace("Bearer ", "");
if (!token) {
ctx.status = 401;
ctx.body = { message: "Unauthorized" };
return; // 不调用 next(),终止执行
}
try {
// 验证 token
const user = await verifyToken(token);
ctx.state.user = user; // 将用户信息存储在 ctx.state 中
await next();
} catch (err) {
ctx.status = 401;
ctx.body = { message: "Invalid token" };
}
}
app.use(auth);条件执行与流程控制
提前返回
async function checkAuth(ctx, next) {
if (!ctx.headers.authorization) {
ctx.status = 401;
ctx.body = { message: "Unauthorized" };
return; // 不调用 next(),后续中间件不会执行
}
await next();
}条件跳过中间件
async function conditionalMiddleware(ctx, next) {
// 只在特定条件下执行
if (ctx.path.startsWith("/api")) {
// 执行某些操作
ctx.state.apiRequest = true;
}
// 无论是否满足条件,都继续执行
await next();
}
// 或者完全跳过
async function skipMiddleware(ctx, next) {
if (shouldSkip(ctx)) {
// 直接跳过,不执行任何逻辑
return next();
}
// 否则执行自定义逻辑
await doSomething(ctx);
await next();
}并行执行
async function parallelMiddleware(ctx, next) {
// 请求阶段:并行执行多个异步操作
const [user, settings, permissions] = await Promise.all([
getUser(ctx.state.userId),
getSettings(ctx.state.userId),
getPermissions(ctx.state.userId),
]);
ctx.state.user = user;
ctx.state.settings = settings;
ctx.state.permissions = permissions;
await next();
}中间件组合与复用
组合多个中间件
const compose = require("koa-compose");
const middleware1 = async (ctx, next) => {
console.log("Middleware 1");
await next();
};
const middleware2 = async (ctx, next) => {
console.log("Middleware 2");
await next();
};
const middleware3 = async (ctx, next) => {
console.log("Middleware 3");
await next();
};
// 组合成单个中间件
const combined = compose([middleware1, middleware2, middleware3]);
app.use(combined);创建中间件工厂
// 创建可配置的中间件
function createAuthMiddleware(options = {}) {
const { required = true, roles = [] } = options;
return async function auth(ctx, next) {
const user = ctx.state.user;
if (required && !user) {
ctx.status = 401;
ctx.body = { message: "Authentication required" };
return;
}
if (roles.length > 0 && !roles.includes(user.role)) {
ctx.status = 403;
ctx.body = { message: "Insufficient permissions" };
return;
}
await next();
};
}
// 使用
app.use(createAuthMiddleware({ required: true, roles: ["admin"] }));路由级中间件
const Router = require("koa-router");
const router = new Router();
// 为特定路由应用中间件
const auth = require("./middleware/auth");
const adminOnly = require("./middleware/admin-only");
router.get("/public", async (ctx) => {
ctx.body = "Public content";
});
router.get("/protected", auth, async (ctx) => {
ctx.body = "Protected content";
});
router.get("/admin", auth, adminOnly, async (ctx) => {
ctx.body = "Admin content";
});
app.use(router.routes());性能优化技巧
避免不必要的异步操作
// ❌ 不必要的 async/await
app.use(async (ctx, next) => {
if (ctx.method === "GET") {
ctx.body = "GET request";
return; // 直接返回,不需要 await
}
await next();
});
// ✅ 优化:只在需要时使用 async
app.use((ctx, next) => {
if (ctx.method === "GET") {
ctx.body = "GET request";
return;
}
return next(); // 直接返回 Promise
});缓存中间件结果
const cache = new Map();
async function cacheMiddleware(ctx, next) {
const key = ctx.url;
// 检查缓存
if (cache.has(key) && ctx.method === "GET") {
ctx.body = cache.get(key);
return; // 不调用 next(),直接返回缓存
}
await next();
// 缓存响应
if (ctx.method === "GET" && ctx.status === 200) {
cache.set(key, ctx.body);
}
}并发控制
let activeRequests = 0;
const MAX_CONCURRENT = 10;
async function concurrencyLimit(ctx, next) {
if (activeRequests >= MAX_CONCURRENT) {
ctx.status = 503;
ctx.body = { message: "Service temporarily unavailable" };
return;
}
activeRequests++;
try {
await next();
} finally {
activeRequests--;
}
}高级应用场景
请求链追踪
const { v4: uuidv4 } = require("uuid");
async function requestId(ctx, next) {
// 生成请求 ID
const requestId = uuidv4();
ctx.state.requestId = requestId;
// 在请求头中添加 ID(用于日志关联)
ctx.set("X-Request-ID", requestId);
await next();
}
// 日志中间件使用请求 ID
async function logger(ctx, next) {
const requestId = ctx.state.requestId;
console.log(`[${requestId}] ${ctx.method} ${ctx.url}`);
await next();
console.log(`[${requestId}] Completed in ${Date.now() - ctx.state.start}ms`);
}数据验证中间件
const Joi = require("joi");
function validate(schema) {
return async function validator(ctx, next) {
try {
// 验证请求体
const value = await schema.validateAsync(ctx.request.body, {
abortEarly: false,
});
// 将验证后的数据替换原始数据
ctx.request.body = value;
await next();
} catch (err) {
if (err.isJoi) {
ctx.status = 400;
ctx.body = {
message: "Validation failed",
errors: err.details.map((detail) => ({
field: detail.path.join("."),
message: detail.message,
})),
};
return;
}
throw err;
}
};
}
// 使用
const createUserSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0),
});
app.use(validate(createUserSchema));中间件执行时间统计
async function timingMiddleware(ctx, next) {
const timings = {};
// 记录开始时间
timings.start = Date.now();
// 包装 next() 以测量每个中间件的执行时间
const originalNext = ctx.next;
let middlewareIndex = 0;
ctx.next = async function (...args) {
const middlewareStart = Date.now();
await originalNext.apply(this, args);
timings[`middleware-${middlewareIndex++}`] = Date.now() - middlewareStart;
};
await next();
timings.total = Date.now() - timings.start;
ctx.set("X-Timing", JSON.stringify(timings));
}常见陷阱与最佳实践
陷阱 1:忘记 await next()
// ❌ 错误:没有 await,无法正确等待后续中间件
app.use((ctx, next) => {
console.log("Before");
next(); // 不会等待
console.log("After"); // 立即执行
});
// ✅ 正确
app.use(async (ctx, next) => {
console.log("Before");
await next(); // 等待后续中间件完成
console.log("After");
});陷阱 2:在 next() 后修改响应
// ❌ 可能的问题:响应可能已经发送
app.use(async (ctx, next) => {
await next();
ctx.body = "Modified"; // 可能无效,因为响应已经发送
});
// ✅ 正确:在调用 next() 之前设置响应
app.use(async (ctx, next) => {
ctx.body = "Original";
await next();
// 后续中间件可以修改 ctx.body
});陷阱 3:错误处理位置
// ❌ 错误:错误处理中间件位置不对
app.use(async (ctx, next) => {
await next();
});
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
// 无法捕获前面中间件的错误
}
});
// ✅ 正确:错误处理放在最外层
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
// 捕获所有中间件的错误
ctx.status = err.status || 500;
ctx.body = { message: err.message };
}
});
app.use(async (ctx, next) => {
// 其他中间件
await next();
});最佳实践
- 总是 await next():除非你确定要终止执行
- 错误处理放在最外层:确保能捕获所有错误
- 使用 ctx.state 传递数据:而不是修改 ctx 的其他属性
- 避免阻塞操作:在中间件中进行 CPU 密集型操作
- 合理使用组合:使用 koa-compose 组合相关中间件
- 中间件应该是纯函数:避免副作用和状态共享
实际案例:构建完整的中间件栈
const Koa = require("koa");
const app = new Koa();
// 1. 错误处理(最外层)
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
error: {
message: err.message,
...(process.env.NODE_ENV === "development" && { stack: err.stack }),
},
};
ctx.app.emit("error", err, ctx);
}
});
// 2. 请求 ID
app.use(async (ctx, next) => {
ctx.state.requestId = require("uuid").v4();
ctx.set("X-Request-ID", ctx.state.requestId);
await next();
});
// 3. 日志
app.use(async (ctx, next) => {
const start = Date.now();
console.log(`[${ctx.state.requestId}] ${ctx.method} ${ctx.url}`);
await next();
const ms = Date.now() - start;
console.log(`[${ctx.state.requestId}] ${ctx.method} ${ctx.url} - ${ms}ms`);
});
// 4. 响应时间
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set("X-Response-Time", `${ms}ms`);
});
// 5. 认证
app.use(async (ctx, next) => {
if (ctx.path.startsWith("/api")) {
const token = ctx.headers.authorization?.replace("Bearer ", "");
if (!token) {
ctx.throw(401, "Unauthorized");
}
ctx.state.user = await verifyToken(token);
}
await next();
});
// 6. 路由处理
app.use(async (ctx) => {
if (ctx.path === "/api/users") {
ctx.body = { users: [] };
} else if (ctx.path === "/api/posts") {
ctx.body = { posts: [] };
} else {
ctx.throw(404, "Not Found");
}
});
app.listen(3000);理解 Koa 洋葱模型是掌握 Koa.js 的关键。通过合理设计中间件执行顺序,可以构建出清晰、可维护、高性能的 Web 应用。记住:洋葱模型的精髓在于双向执行,让每个中间件都有机会在请求的完整生命周期中发挥作用。