【Koa.js】 第二课:中间件机制详解

【Koa.js】 第二课:中间件机制详解

✉ Koa.js 第二课:中间件机制详解

🎯 课程目标

通过本节课的学习,你将能够:

  • 深入理解 Koa 中间件的概念和执行机制
  • 掌握洋葱模型(Onion Model)的工作原理
  • 熟练使用 async/await 处理异步操作
  • 理解 ctx 对象的使用方法
  • 掌握中间件注册顺序与执行顺序的关系
  • 实现自定义中间件和错误处理机制

📘 引言

❓什么是中间件?

中间件(Middleware)是 Koa 应用的核心概念,它是处理 HTTP 请求和响应的函数。每个中间件都可以访问和修改请求和响应对象,以及调用下一个中间件。在 Koa 中,中间件通过 app.use() 方法注册。

❓为什么需要中间件?

中间件提供了一种模块化的方式来处理请求,每个中间件负责特定的功能,如日志记录、身份验证、错误处理等。这种设计使得应用更加灵活、可维护和可扩展。

❗Koa 中间件的特点

  1. 洋葱模型:Koa 的中间件执行遵循洋葱模型,提供了更精细的控制能力
  2. 异步支持:充分利用 async/await 处理异步操作
  3. 上下文共享:通过 ctx 对象在中间件间共享数据
  4. 组合性:中间件可以轻松组合和复用

🧠 重难点分析

重点内容

  • ✅ 洋葱模型的理解和应用
  • ✅ 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);
  };
}

📝 总结

本节课要点回顾

  1. 洋葱模型

    • 理解中间件的执行顺序
    • 掌握 next() 的调用时机
    • 利用前置和后置处理
  2. async/await 使用

    • 正确处理异步操作
    • 避免回调地狱
    • 统一错误处理
  3. ctx 对象

    • 访问请求和响应数据
    • 在中间件间共享状态
    • 设置响应头和状态码
  4. 自定义中间件

    • 封装可复用的功能
    • 遵循中间件设计模式
    • 提供配置选项
  5. 错误处理

    • 全局错误捕获
    • 区分操作性错误和编程错误
    • 根据环境返回适当错误信息

下一步学习建议

  1. 深入学习路由中间件:掌握 @koa/router 的使用
  2. 研究常用中间件:koa-bodyparser、koa-static 等
  3. 实现复杂业务逻辑:结合数据库操作
  4. 学习测试中间件:编写单元测试和集成测试
  5. 优化中间件性能:减少不必要的操作

课后练习

  1. 实现一个记录用户访问日志的中间件
  2. 创建一个限制请求频率的中间件
  3. 编写一个处理文件上传的中间件
  4. 实现一个支持多种认证方式的中间件
  5. 创建一个根据用户角色控制访问权限的中间件

🎄通过本节课的学习,你已经深入理解了 Koa 的中间件机制和洋葱模型,这为构建复杂的应用程序奠定了坚实的基础。下一节课我们将学习 Koa 的路由管理。

转载请说明出处内容投诉
CSS教程网 » 【Koa.js】 第二课:中间件机制详解

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买