koa 的中间件机制
#koa
#nodejs
目录
1. 中间件基本概念
Koa 的中间件是一个函数,它接收两个参数:
ctx
:上下文对象,包含请求和响应信息next
:下一个中间件函数
1.1. 基本结构
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
// 中间件逻辑
await next();
});
1.2. 洋葱模型
app.use(async (ctx, next) => {
console.log('1. 进入第一层');
await next();
console.log('6. 离开第一层');
});
app.use(async (ctx, next) => {
console.log('2. 进入第二层');
await next();
console.log('5. 离开第二层');
});
app.use(async (ctx, next) => {
console.log('3. 进入第三层');
ctx.body = 'Hello World';
console.log('4. 离开第三层');
});
输出顺序:
1. 进入第一层
2. 进入第二层
3. 进入第三层
4. 离开第三层
5. 离开第二层
6. 离开第一层
2. 常用中间件类型
2.1. 错误处理中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
code: ctx.status,
message: err.message,
error: process.env.NODE_ENV === 'development' ? err.stack : undefined
};
// 触发应用级错误事件
ctx.app.emit('error', err, ctx);
}
});
2.2. 日志中间件
app.use(async (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} ${ctx.status} ${ms}ms`);
});
2.3. 认证中间件
const jwt = require('jsonwebtoken');
const auth = async (ctx, next) => {
try {
const token = ctx.header.authorization?.split(' ')[1];
if (!token) {
ctx.throw(401, '未提供认证令牌');
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ctx.state.user = decoded;
await next();
} catch (err) {
ctx.throw(401, '认证失败');
}
};
// 使用中间件
app.use(auth);
2.4. 响应格式化中间件
app.use(async (ctx, next) => {
await next();
// 统一响应格式
if (ctx.body) {
ctx.body = {
code: ctx.status === 200 ? 0 : ctx.status,
data: ctx.body,
message: ctx.message || 'success',
timestamp: new Date().toISOString()
};
}
});
3. 高级中间件模式
3.1. 条件中间件
const unless = require('koa-unless');
const authMiddleware = async (ctx, next) => {
// 认证逻辑
await next();
};
// 添加除外路径
authMiddleware.unless = unless;
app.use(authMiddleware.unless({
path: [/^\/public/, '/login', '/register']
}));
3.2. 组合中间件
const compose = require('koa-compose');
const middlewares = compose([
async (ctx, next) => {
// 中间件1
await next();
},
async (ctx, next) => {
// 中间件2
await next();
}
]);
app.use(middlewares);
3.3. 参数化中间件
const rateLimit = (options = {}) => {
const { interval = 60000, max = 100 } = options;
const store = new Map();
return async (ctx, next) => {
const key = ctx.ip;
const now = Date.now();
const requests = store.get(key) || [];
// 清理过期请求记录
const validRequests = requests.filter(time => now - time < interval);
if (validRequests.length >= max) {
ctx.throw(429, '请求过于频繁');
}
validRequests.push(now);
store.set(key, validRequests);
await next();
};
};
// 使用中间件
app.use(rateLimit({ interval: 60000, max: 100 }));
4. 常用第三方中间件
4.1. 请求体解析 (koa-bodyparser)
const bodyParser = require('koa-bodyparser');
app.use(bodyParser({
enableTypes: ['json', 'form'],
jsonLimit: '5mb',
formLimit: '5mb'
}));
4.2. 静态文件服务 (koa-static)
const serve = require('koa-static');
const path = require('path');
app.use(serve(path.join(__dirname, 'public')));
4.3. 路由 (koa-router)
const Router = require('koa-router');
const router = new Router();
router.get('/', async (ctx) => {
ctx.body = 'Hello World';
});
router.post('/users', async (ctx) => {
// 创建用户
});
app.use(router.routes());
app.use(router.allowedMethods());
4.4. CORS (koa-cors)
const cors = require('@koa/cors');
app.use(cors({
origin: '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization']
}));
5. 最佳实践
5.1. 中间件封装
// middleware/logger.js
module.exports = (options = {}) => {
return async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
if (options.console) {
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
}
if (options.headerEnabled) {
ctx.set('X-Response-Time', `${ms}ms`);
}
};
};
5.2. 异步错误处理
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
if (err.isJoi) { // 验证错误
ctx.status = 400;
ctx.body = {
code: 400,
message: '参数验证失败',
details: err.details
};
} else if (err.name === 'UnauthorizedError') { // JWT 错误
ctx.status = 401;
ctx.body = {
code: 401,
message: '认证失败'
};
} else {
// 其他错误
ctx.status = err.status || 500;
ctx.body = {
code: ctx.status,
message: err.message
};
}
// 记录错误日志
console.error(err);
}
});
5.3. 中间件配置管理
// config/middleware.js
module.exports = {
cors: {
enabled: true,
options: {
origin: '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE']
}
},
bodyParser: {
enabled: true,
options: {
enableTypes: ['json', 'form'],
jsonLimit: '5mb'
}
},
// 其他中间件配置
};
// app.js
const middlewareConfig = require('./config/middleware');
if (middlewareConfig.cors.enabled) {
app.use(cors(middlewareConfig.cors.options));
}
if (middlewareConfig.bodyParser.enabled) {
app.use(bodyParser(middlewareConfig.bodyParser.options));
}
记住要始终考虑中间件的执行顺序,并确保正确处理异步操作和错误。
6. next()
前后代码的区别和特点:
6.1. 代码执行顺序示例
让我们通过一个具体的例子来说明:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('1. next() 之前的代码');
const startTime = Date.now(); // next() 之前的代码
await next(); // 分界线
const endTime = Date.now(); // next() 之后的代码
console.log('4. next() 之后的代码');
console.log(`请求耗时: ${endTime - startTime}ms`);
});
app.use(async (ctx, next) => {
console.log('2. 第二个中间件开始');
await next();
console.log('3. 第二个中间件结束');
});
app.use(async (ctx) => {
console.log('→ 到达最内层的中间件');
ctx.body = 'Hello World';
});
执行顺序将会是:
1. next() 之前的代码
2. 第二个中间件开始
→ 到达最内层的中间件
3. 第二个中间件结束
4. next() 之后的代码
请求耗时: XXms
6.2. next() 前后代码的主要区别
6.2.1. next() 之前的代码:==请求阶段==
-
请求阶段(Downstream)特点:
- 按照中间件注册的顺序从外到内执行
- 适合做预处理工作
- 可以==阻止请求继续向下传递==
- 常见使用场景:
app.use(async (ctx, next) => { // next() 之前:预处理 ctx.state.startTime = Date.now(); // 记录开始时间 ctx.state.requestId = generateId(); // 生成请求ID const token = ctx.headers.authorization; if (!token) { ctx.status = 401; return; // 如果没有token,直接返回,不调用next() } await next(); });
-
常见用途:
- 权限验证
- 参数验证
- 请求日志记录开始
- 设置公共变量
- 请求预处理
6.2.2. next() 之后的代码:==响应阶段==
-
响应阶段(Upstream)特点:
- 按照中间件注册的顺序从内到外执行
- 适合做后处理工作
- 可以==修改响应数据==
- 常见使用场景:
app.use(async (ctx, next) => { try { await next(); // next() 之后:后处理 const duration = Date.now() - ctx.state.startTime; // 修改响应头 ctx.set('X-Response-Time', `${duration}ms`); // 对响应进行处理 if (ctx.body && typeof ctx.body === 'object') { ctx.body = { code: 0, data: ctx.body, requestId: ctx.state.requestId }; } } catch (err) { // 错误处理 ctx.status = err.status || 500; ctx.body = { error: err.message }; } });
-
常见用途:
- 响应格式化
- 响应时间统计
- 错误处理
- 日志记录完成
- 清理临时资源
6.3. 实际应用示例
6.3.1. 完整的日志中间件
app.use(async (ctx, next) => {
// next() 之前:记录请求信息
const startTime = Date.now();
console.log(`[请求开始] ${ctx.method} ${ctx.url}`);
console.log('请求头:', ctx.headers);
try {
await next();
// next() 之后:记录响应信息
const duration = Date.now() - startTime;
console.log(`[请求完成] ${ctx.method} ${ctx.url}`);
console.log(`响应状态: ${ctx.status}`);
console.log(`响应时间: ${duration}ms`);
} catch (err) {
// 错误处理
const duration = Date.now() - startTime;
console.error(`[请求错误] ${ctx.method} ${ctx.url}`);
console.error('错误信息:', err);
console.log(`响应时间: ${duration}ms`);
throw err;
}
});
6.3.2. 响应格式化中间件
app.use(async (ctx, next) => {
// next() 之前:准备工作
ctx.state.requestId = Math.random().toString(36).substring(7);
await next();
// next() 之后:统一响应格式
if (ctx.body) {
ctx.body = {
code: ctx.status === 200 ? 0 : ctx.status,
data: ctx.body,
requestId: ctx.state.requestId,
timestamp: new Date().toISOString()
};
}
});
6.4. 注意事项
-
异步处理:
- 必须使用
await next()
确保异步操作按序执行 - 不要忘记处理异步错误
- 必须使用
-
状态共享:
- 使用
ctx.state
在中间件之间共享数据 next()
前设置的数据在后续中间件中可用
- 使用
-
错误处理:
- 在外层中间件使用
try-catch
捕获错误 - 可以在
next()
后统一处理错误响应
- 在外层中间件使用
-
性能考虑:
- next() 前后的代码都会影响请求处理时间
- 避免在中间件中进行重复的耗时操作
7. 中间件的顺序
不同的顺序可能会导致完全不同的结果
7.1. 中间件顺序的基本原则
7.1.1. 从外到内的顺序建议:
- 错误处理中间件 → 最外层
- 全局通用中间件 → 第二层
- 第三方功能中间件 → 第三层
- 业务相关中间件 → 最内层
让我们通过一个示例来说明:
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const cors = require('@koa/cors');
const compress = require('koa-compress');
const app = new Koa();
// 1. 错误处理中间件 - 最外层
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = { error: err.message };
// 触发应用级错误事件
ctx.app.emit('error', err, ctx);
}
});
// 2. 全局通用中间件
app.use(async (ctx, next) => {
const start = Date.now();
await next();
console.log(`${ctx.method} ${ctx.url} - ${Date.now() - start}ms`);
});
// 3. 第三方功能中间件
app.use(cors()); // 处理跨域
app.use(compress()); // 压缩响应
app.use(bodyParser()); // 解析请求体
// 4. 业务相关中间件
app.use(async (ctx, next) => {
// 验证用户身份
await auth(ctx, next);
});
// 5. 路由中间件
app.use(router.routes());
7.2. 不同类型中间件的顺序说明
7.2.1. 错误处理中间件(最外层)
// 应该放在最外层
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
// 错误处理逻辑
}
});
原因:
- ==可以捕获所有其他中间件中的错误==
- 统一的错误处理机制
- 防止错误导致应用崩溃
7.2.2. 全局通用中间件(第二层)
// 请求日志
app.use(async (ctx, next) => {
console.log(`${ctx.method} ${ctx.url} - 开始`);
await next();
console.log(`${ctx.method} ${ctx.url} - 结束`);
});
// 响应时间统计
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
原因:
- 需要记录所有请求的信息
- 不依赖于其他中间件的处理结果
7.2.3. 第三方功能中间件(第三层)
// 处理跨域
app.use(cors({
origin: '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE']
}));
// 解析请求体
app.use(bodyParser({
enableTypes: ['json', 'form']
}));
// 静态文件服务
app.use(serve('./public'));
原因:
- 为后续中间件提供基础功能
- 相互之间可能有依赖关系
7.2.4. 业务相关中间件(内层)
// 用户认证
app.use(async (ctx, next) => {
const token = ctx.headers.authorization;
if (token) {
ctx.state.user = await verifyToken(token);
}
await next();
});
// 权限检查
app.use(async (ctx, next) => {
if (!ctx.state.user) {
ctx.throw(401, '未授权');
}
await next();
});
原因:
- 依赖前面中间件的处理结果
- 针对特定业务场景
7.3. 特殊情况的顺序考虑
7.3.1. 性能相关中间件
// 压缩应该在返回具体内容之前
app.use(compress());
// 缓存中间件
app.use(async (ctx, next) => {
const cacheKey = `cache:${ctx.url}`;
const cached = await redis.get(cacheKey);
if (cached) {
ctx.body = cached;
return;
}
await next();
// 存储缓存
await redis.set(cacheKey, ctx.body);
});
7.3.2. 安全相关中间件
// 安全头设置应该尽早
app.use(async (ctx, next) => {
ctx.set('X-Frame-Options', 'DENY');
ctx.set('X-Content-Type-Options', 'nosniff');
await next();
});
// CSRF 保护
app.use(csrf());
7.4. 常见问题和解决方案
7.4.1. 中间件冲突
// 错误示例
app.use(bodyParser()); // 解析 JSON
app.use(async (ctx, next) => {
// 直接读取原始请求体
const raw = await getRawBody(ctx.req); // 可能失败
await next();
});
// 正确示例
app.use(async (ctx, next) => {
// 需要原始请求体的处理
const raw = await getRawBody(ctx.req);
ctx.state.rawBody = raw;
await next();
});
app.use(bodyParser()); // 之后再解析 JSON
7.4.2. 条件中间件
// 根据条件决定是否执行某个中间件
const conditionalMiddleware = (condition) => {
return async (ctx, next) => {
if (condition) {
// 执行特定逻辑
await someMiddleware(ctx, next);
} else {
await next();
}
};
};
8. 注意事项
8.1. 避免多次调用 next()
app.use(async (ctx, next) => {
await next(); // 只调用一次
// await next(); // 错误:不要多次调用
});
8.2. 正确的顺序安排
- 错误处理中间件应该放在最前面
- 请求处理中间件按照依赖关系排序
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: err.message };
}
});
// 2. 通用中间件(如日志)
app.use(async (ctx, next) => {
const start = Date.now();
await next();
console.log(`${ctx.method} ${ctx.url} - ${Date.now() - start}ms`);
});
// 3. 第三方中间件
app.use(bodyParser());
app.use(cors());
// 4. 业务中间件
app.use(router.routes());
8.3. 避免顺序错误
// ❌ 错误示例
app.use(router.routes()); // 路由放在了错误处理前面
app.use(errorHandler); // 错误处理无法捕获路由中的错误
// ✅ 正确示例
app.use(errorHandler); // 先注册错误处理
app.use(router.routes()); // 再注册路由
8.4. 始终使用 await
// ❌ 错误示例
app.use(async (ctx, next) => {
doAsyncOperation(); // 未使用 await,异步操作可能不会完成
next(); // 未使用 await,可能导致洋葱模型失效
});
// ✅ 正确示例
app.use(async (ctx, next) => {
await doAsyncOperation();
await next();
});
8.5. 正确的错误处理
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
// 确保错误被正确处理
ctx.status = err.status || 500;
ctx.app.emit('error', err, ctx);
throw err; // 继续向上传递错误
}
});
8.6. 正确使用 ctx.state → 在中间件间共享数据
// ✅ 正确示例:使用 ctx.state 在中间件间共享数据
app.use(async (ctx, next) => {
ctx.state.user = await getUser(ctx.header.authorization);
await next();
});
app.use(async (ctx, next) => {
// 在后续中间件中使用
const user = ctx.state.user;
await next();
});
8.7. 避免修改原始对象
// ❌ 错误示例:直接修改原始请求对象
app.use(async (ctx, next) => {
ctx.req.body = await parseBody(ctx.req);
await next();
});
// ✅ 正确示例:使用 ctx 提供的属性
app.use(async (ctx, next) => {
ctx.request.body = await parseBody(ctx.req);
await next();
});
8.8. 避免不必要的异步操作
app.use(async (ctx, next) => {
// ✅ 正确示例:条件判断避免不必要的操作
if (ctx.path === '/api' && !ctx.state.user) {
ctx.throw(401);
return;
}
await next();
});
8.9. 合理使用缓存
app.use(async (ctx, next) => {
const cacheKey = `cache:${ctx.url}`;
const cached = await redis.get(cacheKey);
if (cached) {
ctx.body = JSON.parse(cached);
return;
}
await next();
// 缓存响应
await redis.set(cacheKey, JSON.stringify(ctx.body), 'EX', 3600);
});
8.10. 请求验证
app.use(async (ctx, next) => {
// 验证请求头
const apiKey = ctx.headers['x-api-key'];
if (!apiKey) {
ctx.throw(401, 'API key required');
}
// 验证请求大小
const contentLength = parseInt(ctx.headers['content-length']);
if (contentLength > 1024 * 1024) { // 1MB
ctx.throw(413, 'Request entity too large');
}
await next();
});
8.11. 响应头安全
app.use(async (ctx, next) => {
await next();
// 设置安全响应头
ctx.set('X-Content-Type-Options', 'nosniff');
ctx.set('X-Frame-Options', 'DENY');
ctx.set('X-XSS-Protection', '1; mode=block');
});
8.12. 确保资源释放
app.use(async (ctx, next) => {
const connection = await db.connect();
try {
ctx.state.db = connection;
await next();
} finally {
// 确保资源被释放
await connection.close();
}
});
8.13. 超时处理
const timeout = ms => new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Request timeout')), ms);
});
app.use(async (ctx, next) => {
try {
await Promise.race([
next(),
timeout(5000) // 5秒超时
]);
} catch (err) {
if (err.message === 'Request timeout') {
ctx.status = 504;
ctx.body = 'Request timeout';
} else {
throw err;
}
}
});
8.14. 添加调试信息
app.use(async (ctx, next) => {
const requestId = uuid();
ctx.state.requestId = requestId;
console.log(`[${requestId}] Request started: ${ctx.method} ${ctx.url}`);
const startTime = Date.now();
try {
await next();
} finally {
console.log(`[${requestId}] Request completed in ${Date.now() - startTime}ms`);
}
});
8.15. 性能监控
app.use(async (ctx, next) => {
const metrics = {
path: ctx.path,
method: ctx.method,
startTime: Date.now()
};
try {
await next();
metrics.status = ctx.status;
} catch (err) {
metrics.error = err.message;
throw err;
} finally {
metrics.duration = Date.now() - metrics.startTime;
// 发送指标到监控系统
await sendMetrics(metrics);
}
});
8.16. 集中式错误处理
// 创建错误处理中间件
const errorHandler = async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
error: {
code: err.code || 'INTERNAL_ERROR',
message: err.message || 'Internal Server Error',
...(process.env.NODE_ENV === 'development' ? {stack: err.stack} : {})
}
};
// 记录错误
ctx.app.emit('error', err, ctx);
}
};
// 使用错误处理中间件
app.use(errorHandler);