中间件概念

中间件是 Express 框架的核心设计模式之一。它提供了一种优雅的方式,在请求-响应周期中插入逻辑,实现诸如日志记录、身份验证、数据解析等功能。本章将深入探讨中间件的概念、类型、执行顺序及如何编写自定义中间件。

1. 什么是中间件?

中间件是一个函数,它可以访问请求对象 (req)、响应对象 (res) 以及应用程序请求-响应循环中的下一个中间件函数 (next)。中间件可以执行以下任务:

  • 执行任何代码。
  • 修改请求和响应对象。
  • 结束请求-响应循环(即发送响应)。
  • 调用堆栈中的下一个中间件。

如果当前中间件没有结束请求-响应循环,则必须调用 next() 将控制权传递给下一个中间件,否则请求会挂起。

2. 中间件的类型

Express 中间件根据应用范围和使用方式可以分为以下几类:

2.1 应用级中间件

使用 app.use()app.METHOD() 绑定的中间件,作用于整个应用或特定路由。

// 所有请求都会经过这个中间件
app.use((req, res, next) => {
  console.log('请求时间:', Date.now());
  next();
});

// 仅对 /user/:id 路径的请求生效
app.use('/user/:id', (req, res, next) => {
  console.log('用户相关请求');
  next();
});

2.2 路由级中间件

绑定到 express.Router() 实例上的中间件,用于模块化路由。用法与应用级中间件相同,但作用范围限于该路由实例。

const router = express.Router();

// 此中间件仅在此路由中生效
router.use((req, res, next) => {
  console.log('路由中间件');
  next();
});

router.get('/', (req, res) => {
  res.send('用户列表');
});

app.use('/users', router);

2.3 错误处理中间件

错误处理中间件有四个参数:(err, req, res, next)。它必须定义在所有其他中间件之后,用于捕获和处理错误。

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('出错了!');
});

2.4 内置中间件

Express 从 4.x 版本开始,除 express.static 外,其他内置中间件都被移除,需要单独安装。常用的内置中间件有:

  • express.json() - 解析 JSON 请求体。
  • express.urlencoded({ extended: true }) - 解析 URL-encoded 请求体。
  • express.static() - 托管静态文件。
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));

2.5 第三方中间件

由社区提供的中间件,需通过 npm 安装后使用,例如 morgan(日志)、cors(跨域)、helmet(安全)等。

const morgan = require('morgan');
const cors = require('cors');

app.use(morgan('dev'));
app.use(cors());

3. 中间件的执行顺序

中间件按照它们在代码中出现的顺序依次执行。如果某个中间件调用了 next(),控制权将传递给下一个匹配的中间件;如果调用 res.send() 等终结方法,则请求结束,后续中间件不会执行。

app.use((req, res, next) => {
  console.log('中间件 1');
  next(); // 传递给下一个
});

app.use((req, res, next) => {
  console.log('中间件 2');
  res.send('Hello'); // 请求结束
});

app.use((req, res, next) => {
  // 不会执行
  console.log('中间件 3');
});

顺序非常重要,例如静态文件中间件应该放在路由之前,而错误处理中间件必须放在最后。

4. 编写自定义中间件

自定义中间件通常是一个接受 reqresnext 参数的函数。下面是一个简单的日志中间件:

const logger = (req, res, next) => {
  const { method, url } = req;
  console.log(`${method} ${url}`);
  next(); // 必须调用以继续
};

app.use(logger);

4.1 带配置的中间件

如果需要传递配置参数,可以编写一个返回中间件函数的函数:

const myLogger = (options) => {
  return (req, res, next) => {
    if (options.enable) {
      console.log('日志已启用');
    }
    next();
  };
};

app.use(myLogger({ enable: true }));

5. 中间件应用示例

5.1 请求时间记录

app.use((req, res, next) => {
  req.requestTime = Date.now(); // 添加到请求对象
  next();
});

app.get('/', (req, res) => {
  res.send(`请求时间: ${req.requestTime}`);
});

5.2 简单的身份验证

const auth = (req, res, next) => {
  const token = req.headers['authorization'];
  if (token && token === 'secret-token') {
    req.user = { name: 'John' };
    next();
  } else {
    res.status(401).send('Unauthorized');
  }
};

// 保护特定路由
app.get('/admin', auth, (req, res) => {
  res.send(`欢迎 ${req.user.name}`);
});

5.3 静态文件服务

使用内置中间件 express.static 托管静态资源:

app.use('/static', express.static(path.join(__dirname, 'public')));

6. 中间件的注意事项

  • 中间件的顺序至关重要,路由和中间件按照定义顺序匹配。
  • 不要在中间件中忘记调用 next() 或发送响应,否则请求会挂起。
  • 错误处理中间件必须定义在所有其他中间件之后,并带有四个参数。
  • 对于异步中间件,确保正确捕获错误并传递给 next(err)
最佳实践:
  • 将通用中间件(如日志、解析器)放在最前面。
  • 使用路由模块化来组织中间件和路由。
  • 为不同的环境(开发、生产)配置不同的中间件(如详细日志只在开发使用)。

7. 异步中间件的错误处理

在异步中间件中,如果发生错误,必须将错误传递给 next,而不是抛出,否则错误不会被捕获。

app.use(async (req, res, next) => {
  try {
    const data = await someAsyncFunction();
    req.data = data;
    next();
  } catch (err) {
    next(err); // 传递给错误处理中间件
  }
});

小结

中间件是 Express 应用的骨架,理解其工作原理对于构建可维护、可扩展的应用至关重要。本章介绍了中间件的概念、类型、顺序和自定义方法。在下一章中,我们将学习如何使用模板引擎和静态文件服务来构建完整的 Web 应用。