大家好,我是 前端架构师 - 大卫。
更多优质内容请关注微信公众号 @前端大卫。
初心为助前端人🚀,进阶路上共星辰✨,
您的点赞👍与关注❤️,是我笔耕不辍的灯💡。
背景
Koa 的 洋葱模型(Onion Model) 是指其 中间件(Middleware) 的执行机制,类似于剥洋葱的过程,即 "先洋葱外层 -> 再洋葱内层 -> 执行核心逻辑 -> 退回外层" 。它的主要优点包括:
1. 更好的控制流
- 通过
await next()
控制中间件的执行顺序,支持 前置处理 和 后置处理,比 Express 的中间件模式更清晰。 - 例如,可以在
next()
之前进行 请求预处理 (如参数校验、日志记录),在next()
之后进行 响应后处理(如格式化输出、错误处理)。
2. 天然支持异步
- Koa 中间件基于
async/await
,避免了回调地狱,使代码更清晰、更易维护。 - 在 Express 中,异步中间件需要手动
next(err)
处理错误,而 Koa 可以直接使用try/catch
捕获异常,统一管理。
3. 错误处理更优雅
- 由于中间件的 洋葱模型 结构,错误可以在最外层捕获,不需要在每个中间件中单独处理。
- 例如,一个全局
try/catch
中间件就能拦截所有错误,返回统一的错误响应。
4. 代码更清晰
- 洋葱模型的执行顺序符合直觉,可以按照 "先执行前置逻辑 -> 进入核心逻辑 -> 执行后置逻辑" 的方式编写代码,避免了 Express 复杂的
next()
处理。
5. 更强的扩展性
- Koa 的中间件 遵循同一个规范 (基于
async/await
),不同中间件可以自由组合 ,减少了 Express 中因next()
处理不当导致的问题。 - 方便封装 通用中间件(如日志、权限校验、错误处理、CORS、gzip 压缩等)。
🌰 示例:Koa 洋葱模型执行顺序
1. 安装 Koa
Koa 需要 Node.js v12.17.0 或更高版本,以支持 ES2015+ 及
async/await
语法。
bash
pnpm i koa
2. 代码示例
javascript
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx, next) => {
console.log("🧅 外层中间件 - 进入");
await next();
console.log("🧅 外层中间件 - 退出");
});
app.use(async (ctx, next) => {
console.log("🔍 内层中间件 - 进入");
await next();
console.log("🔍 内层中间件 - 退出");
});
app.use(async (ctx) => {
console.log("🎯 核心逻辑 - 处理请求");
ctx.body = "Hello Koa";
});
app.listen(3000, () => console.log("Server running on http://localhost:3000"));
执行顺序(洋葱模型) 如下:
🧅 外层中间件 - 进入
🔍 内层中间件 - 进入
🎯 核心逻辑 - 处理请求
🔍 内层中间件 - 退出
🧅 外层中间件 - 退出
模拟实现
我们将用不到 10 行代码,来实现一个简化版的 Koa 洋葱模型!
JS 版本
去掉额外的 }
,整个 MiddlewareRunner
代码精简至 不到 10 行!
核心逻辑在这一行:middleware(() => this.dispatch(i + 1))
。
这里传递给 middleware
的是 next
函数,本质上是一个递归调用,它会依次执行下一个中间件,直到全部完成。
js
class MiddlewareRunner {
#middlewares = [];
use(middleware) {
this.#middlewares.push(middleware);
}
async dispatch(i = 0) {
const middleware = this.#middlewares[i];
if (!middleware) return;
await middleware(() => this.dispatch(i + 1));
}
}
TS 版本
在 TS 版本中,我们为 middleware
添加了类型约束,以确保它是一个 async
函数,并且接收 next
作为参数。
ts
type Middleware = (next: () => Promise<void>) => Promise<void>;
class MiddlewareRunner {
#middlewares: Middleware[] = [];
use(middleware: Middleware): void {
this.#middlewares.push(middleware);
}
async dispatch(i = 0): Promise<void> {
const middleware = this.#middlewares[i];
if (!middleware) return;
await middleware(() => this.dispatch(i + 1));
}
}
模拟中间件执行
下面,我们来测试这个简化版 Koa。
js
const app = new MiddlewareRunner();
app.use(async (next) => {
console.log("🧅 外层中间件 - 进入");
await next();
console.log("🧅 外层中间件 - 退出");
});
app.use(async (next) => {
console.log("🔍 内层中间件 - 进入");
await next();
console.log("🔍 内层中间件 - 退出");
});
app.use(async () => {
console.log("🎯 核心逻辑 - 处理请求");
});
app.dispatch();
执行顺序
🧅 外层中间件 - 进入
🔍 内层中间件 - 进入
🎯 核心逻辑 - 处理请求
🔍 内层中间件 - 退出
🧅 外层中间件 - 退出
可以看到,整个执行顺序完全符合 Koa 的 洋葱模型:
- 先从外到内,依次执行中间件的 "进入" 逻辑。
- 核心逻辑执行完成后,再从内到外,依次执行中间件的 "退出" 逻辑。
Koa 其他实际应用场景
前置逻辑(如权限校验、日志记录)
1. 权限校验 🔐
在 next()
之前,检查用户是否具备访问权限。如果没有权限,返回 403,并且不执行后续中间件。
js
app.use(async (next) => {
const user = { id: 1, role: "guest" }; // 模拟用户数据
console.log(`👤 当前用户:ID=${user.id}, 角色=${user.role}`);
if (user.role !== "admin") {
console.log("❌ 访问被拒绝:需要管理员权限");
return; // 终止请求,后续中间件不会执行
}
console.log("✅ 权限校验通过,继续处理请求");
await next();
});
📌 说明:
user
模拟的是当前登录的用户信息。- 如果用户角色不是
admin
,则直接拦截请求,避免执行不必要的逻辑。 - 适用于: 管理后台接口,只允许管理员访问。
2. 请求日志记录 📝
在 next()
之前,记录请求的详细信息(请求方法、路径、IP 地址等)。
js
app.use(async (next) => {
const requestTime = new Date().toISOString();
console.log("-----------------------------------");
console.log(`📅 请求时间:${requestTime}`);
console.log(`📌 请求路径:${"/api/data"}`);
console.log(`🔍 请求方法:${"GET"}`);
console.log(`🌍 客户端 IP:${"192.168.1.100"}`);
await next(); // 继续执行下一个中间件
console.log(`✅ 请求处理完成,响应时间:${new Date().toISOString()}`);
});
📌 说明:
- 记录 请求路径、方法、IP 地址、时间 ,方便后续日志分析。
next()
之后,再记录请求完成的时间,用于性能分析。- 适用于: API 日志、性能分析、流量监控。
后置逻辑(如格式化响应、错误处理)
1. 统一格式化 API 响应 🎯
在 next()
之后,封装 API 响应结构,确保所有返回的数据格式一致。
js
app.use(async (next) => {
await next(); // 先执行核心逻辑
const responseData = {
code: 200,
message: "请求成功",
data: { id: 1, name: "商品 A", price: 99 },
};
console.log("📦 格式化响应数据:", JSON.stringify(responseData, null, 2));
});
📌 说明:
next()
之后,将数据包装成统一格式{ code, message, data }
。- 统一格式化数据,避免不同接口返回不同格式,增加前端解析负担。
- 适用于: RESTful API 规范,确保所有接口返回一致。
2. 统一错误处理 ❌
在 next()
之后,捕获所有中间件和核心逻辑中的异常,防止接口崩溃。
js
app.use(async (next) => {
try {
await next();
} catch (error) {
console.error("🚨 捕获到错误:", error.message);
console.log("⚠️ 统一返回错误响应 { code: 500, message: '服务器内部错误' }");
}
});
📌 说明:
try-catch
捕获next()
里的错误,避免接口直接崩溃。- 如果代码中抛出
throw new Error("XXX")
,这个中间件可以拦截并统一返回 500 错误。 - 适用于: 防止 API 500 直接暴露内部错误信息。
总结
- 这个简化版的 Koa 不到 10 行代码,但完全复现了 Koa 的中间件执行机制。
- 洋葱模型的执行方式,让我们可以在
next()
之前进行 前置逻辑 (如权限校验、日志记录),在next()
之后进行 后置逻辑(如格式化响应、错误处理)。 - 通过
async/await
让代码 结构清晰,执行流可控 ,比传统的callback
方式更易读、更易维护。
如果你还没有用过 Koa,希望这个小例子能帮助你理解它的 核心机制! 🚀