✉ Koa.js 第二课:中间件机制详解
🎯 课程目标
通过本节课的学习,你将能够:
- 深入理解 Koa 中间件的概念和执行机制
- 掌握洋葱模型(Onion Model)的工作原理
- 熟练使用 async/await 处理异步操作
- 理解 ctx 对象的使用方法
- 掌握中间件注册顺序与执行顺序的关系
- 实现自定义中间件和错误处理机制
📘 引言
❓什么是中间件?
中间件(Middleware)是 Koa 应用的核心概念,它是处理 HTTP 请求和响应的函数。每个中间件都可以访问和修改请求和响应对象,以及调用下一个中间件。在 Koa 中,中间件通过 app.use() 方法注册。
❓为什么需要中间件?
中间件提供了一种模块化的方式来处理请求,每个中间件负责特定的功能,如日志记录、身份验证、错误处理等。这种设计使得应用更加灵活、可维护和可扩展。
❗Koa 中间件的特点
- 洋葱模型:Koa 的中间件执行遵循洋葱模型,提供了更精细的控制能力
- 异步支持:充分利用 async/await 处理异步操作
- 上下文共享:通过 ctx 对象在中间件间共享数据
- 组合性:中间件可以轻松组合和复用
🧠 重难点分析
重点内容
- ✅ 洋葱模型的理解和应用
- ✅ async/await 在中间件中的正确使用
- ✅ ctx 对象的属性和方法
- ✅ 中间件注册和执行顺序
- ✅ 错误处理中间件的实现
难点内容
- ⚠️ 洋葱模型的执行流程理解
- ⚠️ 中间件中 next() 的调用时机
- ⚠️ 异步操作在中间件中的处理
- ⚠️ 错误传播和捕获机制
🧩 洋葱模型详解
什么是洋葱模型?
洋葱模型是 Koa 中间件的执行机制,它形象地描述了中间件的执行顺序。每个中间件都会被调用两次:在处理请求时从外到内,然后在返回响应时从内到外,就像穿过洋葱的每一层。
// 洋葱模型示意图
// ┌────────────────────────────────────────────────────────────┐
// │ Koa Middleware │
// │ ┌────────────────────────────────────────┐ │
// │ │ middleware1 │ │
// │ │ ┌──────────────────────────────┐ │ │
// │ │ │ middleware2 │ │ │
// │ │ │ ┌────────────────────┐ │ │ │
// │ │ │ │ middleware3 │ │ │ │
// │ │ │ │ │ │ │ │
// │ │ │ └────────────────────┘ │ │ │
// │ │ │ │ │ │ │
// │ │ │ next() │ │ │
// │ │ │ ↓ │ │ │
// │ │ │ ┌────────────────────┐ │ │ │
// │ │ │ │ Response │ │ │ │
// │ │ │ └────────────────────┘ │ │ │
// │ │ │ │ │ │ │
// │ │ │ next() │ │ │
// │ │ └──────────────────────────────┘ │ │
// │ │ │ │ │
// │ │ next() │ │
// │ └────────────────────────────────────────┘ │
// │ │ │
// │ next() │
// └────────────────────────────────────────────────────────────┘
洋葱模型执行示例
const Koa = require('koa');
const app = new Koa();
// 中间件 1
app.use(async (ctx, next) => {
console.log('1 开始');
await next();
console.log('1 结束');
});
// 中间件 2
app.use(async (ctx, next) => {
console.log('2 开始');
await next();
console.log('2 结束');
});
// 中间件 3
app.use(async (ctx, next) => {
console.log('3 处理');
ctx.body = 'Hello Koa';
});
app.listen(3000);
执行结果:
1 开始
2 开始
3 处理
2 结束
1 结束
💻 完整代码实现
✅1. 基础中间件示例
// middleware-demo.js
const Koa = require('koa');
const app = new Koa();
/**
* 1. 日志记录中间件
* 记录请求方法、URL 和处理时间
*/
app.use(async (ctx, next) => {
const start = Date.now();
console.log(`→ ${ctx.method} ${ctx.url}`);
try {
await next();
const ms = Date.now() - start;
console.log(`← ${ctx.method} ${ctx.url} ${ctx.status} ${ms}ms`);
} catch (err) {
const ms = Date.now() - start;
console.error(`✗ ${ctx.method} ${ctx.url} ${err.status || 500} ${ms}ms`);
throw err;
}
});
/**
* 2. 响应时间中间件
* 在响应头中添加 X-Response-Time
*/
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
/**
* 3. 错误处理中间件
* 捕获并处理应用中的错误
*/
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
message: err.message,
status: ctx.status,
...(process.env.NODE_ENV === 'development' ? { stack: err.stack } : {})
};
console.error('Application Error:', err);
ctx.app.emit('error', err, ctx);
}
});
/**
* 4. 身份验证中间件示例
* 检查请求头中的 Authorization
*/
app.use(async (ctx, next) => {
// 模拟身份验证
const auth = ctx.headers.authorization;
if (ctx.path.startsWith('/api/protected')) {
if (!auth || !auth.startsWith('Bearer ')) {
ctx.status = 401;
ctx.body = { error: 'Unauthorized' };
return;
}
// 这里可以添加实际的 token 验证逻辑
console.log('Token verified:', auth.substring(7));
}
await next();
});
/**
* 5. 请求体解析中间件模拟
* 解析 JSON 格式的请求体
*/
app.use(async (ctx, next) => {
if (ctx.method === 'POST' || ctx.method === 'PUT') {
// 模拟解析请求体
const chunks = [];
for await (const chunk of ctx.req) {
chunks.push(chunk);
}
const body = Buffer.concat(chunks).toString();
try {
ctx.request.body = JSON.parse(body);
} catch (err) {
ctx.request.body = body;
}
}
await next();
});
/**
* 6. 核心业务逻辑中间件
* 处理具体的路由和业务逻辑
*/
app.use(async (ctx, next) => {
// 主页路由
if (ctx.path === '/' && ctx.method === 'GET') {
ctx.status = 200;
ctx.type = 'text/html';
ctx.body = `
<!DOCTYPE html>
<html>
<head>
<title>Koa 中间件演示</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.endpoint { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
code { background: #f5f5f5; padding: 2px 4px; border-radius: 3px; }
</style>
</head>
<body>
<h1>Koa 中间件机制演示</h1>
<p>查看控制台输出以了解中间件执行顺序</p>
<div class="endpoint">
<h3>GET /</h3>
<p>当前页面</p>
</div>
<div class="endpoint">
<h3>GET /api/public</h3>
<p>公开 API 接口</p>
<p><a href="/api/public">访问接口</a></p>
</div>
<div class="endpoint">
<h3>GET /api/protected</h3>
<p>受保护的 API 接口</p>
<p>需要 Authorization 头</p>
<p><a href="/api/protected">访问接口</a> (会返回 401)</p>
</div>
<div class="endpoint">
<h3>POST /api/data</h3>
<p>数据提交接口</p>
<p>可以发送 JSON 数据</p>
<p>示例: <code>curl -X POST -H "Content-Type: application/json" -d '{"name":"test"}' http://localhost:3000/api/data</code></p>
</div>
<div class="endpoint">
<h3>GET /error</h3>
<p>触发错误处理</p>
<p><a href="/error">触发错误</a></p>
</div>
</body>
</html>
`;
return;
}
// 公开 API
if (ctx.path === '/api/public' && ctx.method === 'GET') {
ctx.status = 200;
ctx.type = 'application/json';
ctx.body = {
message: '这是公开的 API 接口',
timestamp: new Date().toISOString()
};
return;
}
// 受保护的 API
if (ctx.path === '/api/protected' && ctx.method === 'GET') {
ctx.status = 200;
ctx.type = 'application/json';
ctx.body = {
message: '这是受保护的 API 接口',
user: 'authenticated_user',
data: 'sensitive_data'
};
return;
}
// 数据提交接口
if (ctx.path === '/api/data' && ctx.method === 'POST') {
ctx.status = 201;
ctx.type = 'application/json';
ctx.body = {
message: '数据接收成功',
receivedData: ctx.request.body,
timestamp: new Date().toISOString()
};
return;
}
// 错误测试接口
if (ctx.path === '/error' && ctx.method === 'GET') {
throw new Error('这是一个测试错误!');
}
// 404 处理
ctx.status = 404;
ctx.type = 'text/html';
ctx.body = `
<!DOCTYPE html>
<html>
<head>
<title>404 Not Found</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin: 40px; }
</style>
</head>
<body>
<h1>404 - 页面未找到</h1>
<p>请求的路径: ${ctx.path}</p>
<a href="/">返回首页</a>
</body>
</html>
`;
});
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
console.log('查看控制台输出以了解中间件执行过程');
});
✅2. 自定义中间件示例
// custom-middleware.js
const Koa = require('koa');
const app = new Koa();
/**
* 自定义中间件 1: 请求 ID 生成器
* 为每个请求生成唯一 ID
*/
function requestId() {
return async (ctx, next) => {
// 生成请求 ID
ctx.state.requestId = Math.random().toString(36).substr(2, 9);
console.log(`[${ctx.state.requestId}] 请求开始: ${ctx.method} ${ctx.url}`);
await next();
console.log(`[${ctx.state.requestId}] 请求结束: ${ctx.status}`);
};
}
/**
* 自定义中间件 2: 响应时间统计
* 统计处理时间并添加到响应头
*/
function responseTime() {
return async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
};
}
/**
* 自定义中间件 3: 请求体大小限制
* 限制请求体大小
*/
function bodySizeLimit(maxSize = 1024 * 1024) { // 默认 1MB
return async (ctx, next) => {
if (['POST', 'PUT', 'PATCH'].includes(ctx.method)) {
const chunks = [];
let size = 0;
for await (const chunk of ctx.req) {
size += chunk.length;
if (size > maxSize) {
ctx.status = 413;
ctx.body = { error: `请求体过大,最大允许 ${maxSize} 字节` };
return;
}
chunks.push(chunk);
}
ctx.request.body = Buffer.concat(chunks);
}
await next();
};
}
/**
* 自定义中间件 4: CORS 处理
* 处理跨域请求
*/
function cors(options = {}) {
const defaults = {
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: [],
credentials: false,
maxAge: 0
};
const opts = { ...defaults, ...options };
return async (ctx, next) => {
// 设置 CORS 头
ctx.set('A***ess-Control-Allow-Origin', opts.origin);
if (opts.credentials) {
ctx.set('A***ess-Control-Allow-Credentials', 'true');
}
if (opts.exposedHeaders.length) {
ctx.set('A***ess-Control-Expose-Headers', opts.exposedHeaders.join(','));
}
// 处理预检请求
if (ctx.method === 'OPTIONS') {
ctx.set('A***ess-Control-Allow-Methods', opts.methods.join(','));
ctx.set('A***ess-Control-Allow-Headers', opts.allowedHeaders.join(','));
if (opts.maxAge) {
ctx.set('A***ess-Control-Max-Age', opts.maxAge.toString());
}
ctx.status = 204;
return;
}
await next();
};
}
// 使用自定义中间件
app.use(requestId());
app.use(responseTime());
app.use(cors());
app.use(bodySizeLimit(1024 * 1024)); // 1MB 限制
// 简单的路由处理
app.use(async (ctx) => {
ctx.body = {
message: 'Hello from Koa with custom middleware!',
requestId: ctx.state.requestId,
timestamp: new Date().toISOString()
};
});
app.listen(3000, () => {
console.log('自定义中间件示例服务器运行在 http://localhost:3000');
});
✅3. 错误处理中间件深入
// error-handling.js
const Koa = require('koa');
const app = new Koa();
/**
* 自定义错误类
*/
class AppError extends Error {
constructor(message, statusCode = 500, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
this.name = this.constructor.name;
// 保持堆栈跟踪
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
/**
* 全局错误处理中间件
*/
function errorHandler() {
return async (ctx, next) => {
try {
await next();
} catch (err) {
// 设置默认状态码
err.statusCode = err.statusCode || err.status || 500;
// 记录错误
console.error('Error:', err);
// 发出错误事件
ctx.app.emit('error', err, ctx);
// 根据环境返回不同详细程度的错误信息
if (process.env.NODE_ENV === 'development') {
ctx.status = err.statusCode;
ctx.body = {
status: 'error',
error: {
message: err.message,
stack: err.stack,
name: err.name
}
};
} else {
// 生产环境隐藏详细错误信息
if (err.isOperational) {
ctx.status = err.statusCode;
ctx.body = {
status: 'error',
message: err.message
};
} else {
// 编程错误或未知错误
ctx.status = 500;
ctx.body = {
status: 'error',
message: 'Internal Server Error'
};
}
}
}
};
}
/**
* 未捕获错误处理
*/
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
console.error(err.name, err.message);
console.error(err.stack);
process.exit(1);
});
process.on('unhandledRejection', (err) => {
console.error('UNHANDLED REJECTION! 💥 Shutting down...');
console.error(err);
process.exit(1);
});
// 使用错误处理中间件
app.use(errorHandler());
// 模拟各种错误情况
app.use(async (ctx, next) => {
if (ctx.path === '/operational-error') {
throw new AppError('这是一个操作性错误', 400);
}
if (ctx.path === '/programming-error') {
throw new Error('这是一个编程错误');
}
if (ctx.path === '/async-error') {
// 模拟异步错误
await new Promise((_, reject) => {
setTimeout(() => reject(new AppError('异步操作失败', 400)), 100);
});
}
await next();
});
// 正常路由
app.use(async (ctx) => {
ctx.body = {
message: '正常响应',
path: ctx.path,
timestamp: new Date().toISOString()
};
});
app.listen(3000, () => {
console.log('错误处理示例服务器运行在 http://localhost:3000');
console.log('测试路径:');
console.log('- /operational-error (操作性错误)');
console.log('- /programming-error (编程错误)');
console.log('- /async-error (异步错误)');
});
🧪 测试中间件
✅1. 基本测试
# 启动服务器
node middleware-demo.js
# 测试各种路由
curl http://localhost:3000/
curl http://localhost:3000/api/public
curl http://localhost:3000/api/protected
curl http://localhost:3000/error
# 测试 POST 请求
curl -X POST \
-H "Content-Type: application/json" \
-d '{"name":"test","value":123}' \
http://localhost:3000/api/data
✅2. 自定义中间件测试
# 启动自定义中间件示例
node custom-middleware.js
# 测试 CORS
curl -H "Origin: http://example.***" \
-H "A***ess-Control-Request-Method: POST" \
-H "A***ess-Control-Request-Headers: X-Requested-With" \
-X OPTIONS \
http://localhost:3000
# 普通请求
curl http://localhost:3000
✅3. 错误处理测试
# 启动错误处理示例
node error-handling.js
# 测试各种错误
curl http://localhost:3000/operational-error
curl http://localhost:3000/programming-error
curl http://localhost:3000/async-error
🔍 中间件最佳实践
✅1. 中间件设计原则
// 好的中间件设计
function goodMiddleware() {
return async (ctx, next) => {
// 前置处理
console.log('Before');
try {
await next();
} catch (err) {
// 错误处理
throw err;
} finally {
// 后置处理(总是执行)
console.log('After');
}
};
}
// 避免的写法
function badMiddleware() {
return async (ctx, next) => {
// 在 next() 之前修改响应体是危险的
ctx.body = 'something'; // ❌
await next();
};
}
✅2. 中间件组合
// 组合多个中间件
function ***poseMiddleware(middlewares) {
return async (ctx, next) => {
let index = -1;
async function dispatch(i) {
if (i <= index) {
throw new Error('next() called multiple times');
}
index = i;
let fn = middlewares[i];
if (i === middlewares.length) fn = next;
if (!fn) return;
try {
await fn(ctx, dispatch.bind(null, i + 1));
} catch (err) {
throw err;
}
}
return dispatch(0);
};
}
📝 总结
本节课要点回顾
-
洋葱模型:
- 理解中间件的执行顺序
- 掌握 next() 的调用时机
- 利用前置和后置处理
-
async/await 使用:
- 正确处理异步操作
- 避免回调地狱
- 统一错误处理
-
ctx 对象:
- 访问请求和响应数据
- 在中间件间共享状态
- 设置响应头和状态码
-
自定义中间件:
- 封装可复用的功能
- 遵循中间件设计模式
- 提供配置选项
-
错误处理:
- 全局错误捕获
- 区分操作性错误和编程错误
- 根据环境返回适当错误信息
下一步学习建议
- 深入学习路由中间件:掌握 @koa/router 的使用
- 研究常用中间件:koa-bodyparser、koa-static 等
- 实现复杂业务逻辑:结合数据库操作
- 学习测试中间件:编写单元测试和集成测试
- 优化中间件性能:减少不必要的操作
课后练习
- 实现一个记录用户访问日志的中间件
- 创建一个限制请求频率的中间件
- 编写一个处理文件上传的中间件
- 实现一个支持多种认证方式的中间件
- 创建一个根据用户角色控制访问权限的中间件
🎄通过本节课的学习,你已经深入理解了 Koa 的中间件机制和洋葱模型,这为构建复杂的应用程序奠定了坚实的基础。下一节课我们将学习 Koa 的路由管理。