从 Express 到 Koa:为什么大厂都在用洋葱模型重构后端?
引言
如果你还在用 Express 写 Node.js 后端,可能已经落后了。
2024 年,无论是阿里的 Egg.js、腾讯的 TSF,还是字节跳动的内部框架,都在做同一件事:用 Koa 的洋葱模型重构中间件系统。这不是跟风,而是 Express 的线性中间件模型在复杂业务场景下真的扛不住了。
本文将深入剖析 Koa 的洋葱模型原理,对比 Express 的线性模型,并给出可直接落地的实战代码。
一、Express 的痛点:为什么线性模型不够用了?
1.1 Express 中间件执行顺序
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log('1. 开始');
next();
console.log('5. 结束'); // 这行不会按预期执行
});
app.use((req, res, next) => {
console.log('2. 中间件');
next();
});
app.get('/', (req, res) => {
console.log('3. 路由处理');
res.send('Hello');
console.log('4. 响应后');
});
实际输出:
1. 开始
2. 中间件
3. 路由处理
4. 响应后
问题在哪?
next()之后的代码(第 5 步)不会执行- Express 一旦进入路由处理,响应发出后流程就结束了
- 你无法在响应后统一处理日志、清理资源、统计耗时
1.2 真实业务场景的困扰
假设你要实现一个 API 耗时统计中间件:
// Express 版本 - 有问题的实现
app.use((req, res, next) => {
const start = Date.now();
next();
// 这里执行不到,因为 res.send() 已经结束了请求
const duration = Date.now() - start;
console.log(`${req.path} 耗时: ${duration}ms`);
});
解决方案? 你需要劫持 res.end,代码变得丑陋:
// Express 的妥协方案
app.use((req, res, next) => {
const start = Date.now();
const originalEnd = res.end.bind(res);
res.end = function(...args) {
const duration = Date.now() - start;
console.log(`${req.path} 耗时: ${duration}ms`);
originalEnd(...args);
};
next();
});
这种猴子补丁的方式既不优雅,也容易引发 bug。
二、Koa 的洋葱模型:优雅的解决方案
2.1 什么是洋葱模型?
Koa 的中间件执行顺序像剥洋葱:先进后出(FILO)。
请求 → 中间件1 → 中间件2 → 中间件3 → 业务逻辑
↑ ↑ ↑
└───────────┴───────────┘
返回时逆序执行
代码示例:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('1. 进入外层中间件');
await next();
console.log('5. 离开外层中间件');
});
app.use(async (ctx, next) => {
console.log('2. 进入中层中间件');
await next();
console.log('4. 离开中层中间件');
});
app.use(async (ctx) => {
console.log('3. 执行业务逻辑');
ctx.body = 'Hello Koa';
});
app.listen(3000);
输出:
1. 进入外层中间件
2. 进入中层中间件
3. 执行业务逻辑
4. 离开中层中间件
5. 离开外层中间件
关键区别:
await next()之后的代码一定会执行- 可以在响应后统一处理逻辑
- 代码结构清晰,符合直觉
2.2 洋葱模型的实现原理
Koa 的核心是一个递归 Promise 链:
// Koa 中间件执行的核心逻辑(简化版)
function compose(middleware) {
return function(context, next) {
let index = -1;
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
// 关键:将 next 包装成 Promise
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0);
};
}
执行流程解析:
dispatch(0)执行第一个中间件- 中间件调用
next()实际上是dispatch(1) - 形成递归,直到所有中间件执行完毕
- Promise 链回溯,执行每个中间件
await next()之后的代码
三、实战:用 Koa 重构 Express 项目
3.1 耗时统计中间件(对比版)
Express 版本(丑陋):
app.use((req, res, next) => {
const start = Date.now();
const originalEnd = res.end.bind(res);
res.end = function(...args) {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} - ${duration}ms`);
originalEnd(...args);
};
next();
});
Koa 版本(优雅):
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // 等待所有后续中间件和业务逻辑执行完毕
const duration = Date.now() - start;
console.log(`${ctx.method} ${ctx.path} - ${duration}ms`);
});
优势:
- 无需劫持任何方法
- 代码量减少 50%
- 可读性大幅提升
3.2 错误统一处理中间件
// error-handler.js
module.exports = async (ctx, next) => {
try {
await next();
} catch (err) {
// 统一错误处理
ctx.status = err.status || 500;
ctx.body = {
success: false,
message: err.message,
code: err.code || 'INTERNAL_ERROR'
};
// 记录错误日志
console.error(`[Error] ${ctx.method} ${ctx.path}:`, err);
// 上报监控系统(如 Sentry)
// sentry.captureException(err);
}
};
使用:
const errorHandler = require('./error-handler');
// 放在最外层,捕获所有错误
app.use(errorHandler);
app.use(otherMiddleware);
3.3 数据库事务中间件
这是洋葱模型最实用的场景之一:
// transaction.js
const { sequelize } = require('./db');
module.exports = async (ctx, next) => {
const transaction = await sequelize.transaction();
ctx.transaction = transaction;
try {
await next();
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
};
业务代码使用:
const router = require('koa-router')();
router.post('/order', async (ctx) => {
const { transaction } = ctx;
// 所有数据库操作自动在事务中
await Order.create({...}, { transaction });
await Inventory.decrement({...}, { transaction });
await Payment.create({...}, { transaction });
ctx.body = { success: true };
});
app.use(transaction);
app.use(router.routes());
关键点:
- 事务在中间件中开启
- 业务逻辑执行完毕后自动提交
- 如果业务逻辑抛错,自动回滚
3.4 JWT 鉴权中间件
// auth.js
const jwt = require('jsonwebtoken');
module.exports = async (ctx, next) => {
const token = ctx.headers.authorization?.replace('Bearer ', '');
if (!token) {
ctx.status = 401;
ctx.body = { message: '未提供 Token' };
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ctx.user = decoded;
await next();
} catch (err) {
ctx.status = 401;
ctx.body = { message: 'Token 无效' };
}
};
路由级使用:
const auth = require('./auth');
// 公开路由
router.post('/login', loginHandler);
// 需要鉴权的路由
router.get('/profile', auth, getProfile);
router.post('/order', auth, createOrder);
四、从 Express 迁移到 Koa 的完整方案
4.1 项目结构调整
express-project/ koa-project/
├── routes/ ├── middlewares/ # 中间件目录
│ ├── user.js │ ├── error.js
│ └── order.js │ ├── auth.js
├── middlewares/ │ ├── logger.js
│ └── auth.js │ └── transaction.js
├── app.js ├── routes/
└── package.json │ ├── user.js
│ └── order.js
├── app.js
└── package.json
4.2 核心代码迁移
Express app.js → Koa app.js:
// app.js (Koa)
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const router = require('./routes');
const errorHandler = require('./middlewares/error');
const logger = require('./middlewares/logger');
const app = new Koa();
// 中间件顺序很重要
app.use(errorHandler); // 最外层捕获错误
app.use(logger); // 日志记录
app.use(bodyParser()); // 解析请求体
app.use(router()); // 路由
app.listen(3000, () => {
console.log('Server running on port 3000');
});
4.3 路由迁移
Express 路由 → Koa 路由:
// routes/user.js (Koa)
const Router = require('koa-router');
const router = new Router({ prefix: '/users' });
const auth = require('../middlewares/auth');
// GET /users
router.get('/', async (ctx) => {
const users = await User.findAll();
ctx.body = { success: true, data: users };
});
// GET /users/:id
router.get('/:id', async (ctx) => {
const user = await User.findByPk(ctx.params.id);
if (!user) {
ctx.status = 404;
ctx.body = { message: '用户不存在' };
return;
}
ctx.body = { success: true, data: user };
});
// POST /users (需要鉴权)
router.post('/', auth, async (ctx) => {
const user = await User.create(ctx.request.body);
ctx.status = 201;
ctx.body = { success: true, data: user };
0 评论
评论区
登录 后参与评论