MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js Express 中间件机制详解与实践

2023-11-027.0k 阅读

什么是 Express 中间件

在深入探讨 Express 中间件机制之前,我们先来明确一下什么是中间件。在 Node.js 的 Express 框架中,中间件是一个函数,它可以访问请求对象(req)、响应对象(res)以及应用程序的请求 - 响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量表示。

中间件函数可以执行以下任务:

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

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

Express 中间件的类型

Express 中的中间件主要分为以下几种类型:

  1. 应用级中间件:绑定到 app 实例上,用于处理整个应用的请求。例如:
const express = require('express');
const app = express();

app.use((req, res, next) => {
  console.log('应用级中间件被调用');
  next();
});

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

在上述代码中,app.use 注册的就是一个应用级中间件。每次请求到达应用时,都会先执行这个中间件的逻辑。

  1. 路由级中间件:绑定到 express.Router() 实例上,专门用于处理特定路由的请求。比如:
const express = require('express');
const app = express();
const router = express.Router();

router.use((req, res, next) => {
  console.log('路由级中间件被调用');
  next();
});

router.get('/', (req, res) => {
  res.send('这是路由级中间件处理的路由');
});

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

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

这里 router.use 注册的中间件只对以 /route 开头的路由起作用。

  1. 错误处理中间件:用于捕获应用中发生的错误。其函数签名有四个参数,形式为 (err, req, res, next)。示例如下:
const express = require('express');
const app = express();

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

app.get('/', (req, res) => {
  throw new Error('模拟错误');
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

当在路由处理函数中抛出错误时,错误处理中间件就会捕获并处理这个错误。

  1. 内置中间件:Express 自带了一些中间件,例如 express.static 用于提供静态文件服务。
const express = require('express');
const app = express();

app.use(express.static('public'));

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

上述代码会将 public 目录下的文件作为静态资源对外提供访问。

  1. 第三方中间件:通过 npm 安装的各种中间件,如 body - parser 用于解析请求体数据。
const express = require('express');
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/data', (req, res) => {
  console.log(req.body);
  res.send('数据已接收');
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

这里使用 body - parser 中间件来解析 JSON 和 URL 编码格式的请求体数据。

Express 中间件的执行顺序

理解 Express 中间件的执行顺序至关重要。中间件按照它们被定义的顺序依次执行。对于应用级中间件,在 app.useapp.METHOD(如 app.getapp.post 等)中定义的中间件,从上到下依次执行。

例如:

const express = require('express');
const app = express();

app.use((req, res, next) => {
  console.log('第一个应用级中间件');
  next();
});

app.use((req, res, next) => {
  console.log('第二个应用级中间件');
  next();
});

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

当请求到达根路径 / 时,会先输出 “第一个应用级中间件”,然后输出 “第二个应用级中间件”,最后返回 “Hello, World!”。

对于路由级中间件,同样按照在 router 中定义的顺序执行。而错误处理中间件通常放在所有其他中间件定义之后,因为它需要捕获前面中间件和路由处理函数中抛出的错误。

Express 中间件的核心机制

Express 的中间件机制基于一个函数调用堆栈。当一个请求到达应用时,Express 会依次调用注册的中间件函数。每个中间件函数都有机会对请求和响应进行处理,然后通过调用 next() 将控制权传递给下一个中间件。

在内部,Express 使用一个数组来存储中间件函数。当请求进来时,Express 会遍历这个数组,依次执行每个中间件函数。如果某个中间件函数没有调用 next(),那么请求 - 响应循环就会在此处停止,除非该中间件已经发送了响应(例如通过 res.sendres.json 等方法)。

例如,我们可以自定义一个简单的 Express 中间件系统来模拟这种机制:

function simpleExpress() {
  const middlewares = [];

  function use(middleware) {
    middlewares.push(middleware);
  }

  function handleRequest(req, res) {
    function next() {
      const currentMiddleware = middlewares.shift();
      if (currentMiddleware) {
        currentMiddleware(req, res, next);
      }
    }
    next();
  }

  return {
    use,
    handleRequest
  };
}

const app = simpleExpress();

app.use((req, res, next) => {
  console.log('第一个自定义中间件');
  next();
});

app.use((req, res, next) => {
  console.log('第二个自定义中间件');
  res.send('自定义中间件模拟成功');
});

const req = {};
const res = {
  send: (data) => {
    console.log('发送响应:', data);
  }
};

app.handleRequest(req, res);

在这个模拟实现中,use 方法用于注册中间件,handleRequest 方法用于处理请求。当 handleRequest 被调用时,会依次执行注册的中间件函数,模拟了 Express 的中间件执行流程。

Express 中间件实践:日志记录中间件

日志记录是应用开发中非常重要的功能。我们可以通过 Express 中间件来实现一个简单的日志记录功能。

const express = require('express');
const app = express();

function logger(req, res, next) {
  console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
  next();
}

app.use(logger);

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

上述代码中,logger 函数就是一个中间件。每次有请求到达应用时,它会在控制台记录请求的时间、方法和 URL。通过将 logger 中间件注册到应用中,我们为整个应用添加了基本的日志记录功能。

Express 中间件实践:身份验证中间件

身份验证是确保只有授权用户可以访问某些资源的关键。下面是一个简单的基于令牌的身份验证中间件示例:

const express = require('express');
const app = express();

// 模拟用户数据
const users = [
  { id: 1, token: 'validToken1' },
  { id: 2, token: 'validToken2' }
];

function authenticate(req, res, next) {
  const token = req.headers['authorization'];
  if (token) {
    const user = users.find(u => u.token === token);
    if (user) {
      next();
    } else {
      res.status(401).send('未授权');
    }
  } else {
    res.status(401).send('未授权');
  }
}

app.get('/protected', authenticate, (req, res) => {
  res.send('这是受保护的资源');
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

在这个示例中,authenticate 中间件检查请求头中的 authorization 字段是否包含有效的令牌。如果令牌有效,请求将被传递到下一个中间件(这里是路由处理函数);否则,返回 401 未授权错误。

Express 中间件实践:压缩中间件

为了减少网络传输的数据量,我们可以使用压缩中间件。compression 是一个常用的第三方中间件用于实现此功能。 首先,安装 compression

npm install compression

然后在 Express 应用中使用:

const express = require('express');
const compression = require('compression');
const app = express();

app.use(compression());

app.get('/', (req, res) => {
  const largeData = 'a'.repeat(100000);
  res.send(largeData);
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

在上述代码中,compression 中间件会自动压缩响应数据,使得传输到客户端的数据量更小,从而提高应用的性能。

中间件中的错误处理

在中间件开发过程中,错误处理是必不可少的。当中间件函数发生错误时,应该将错误传递给错误处理中间件。如果没有错误处理中间件,Node.js 会抛出未捕获的异常,导致应用崩溃。

例如,在一个解析 JSON 数据的中间件中可能会发生错误:

const express = require('express');
const app = express();

function jsonParser(req, res, next) {
  try {
    req.body = JSON.parse(req.body);
    next();
  } catch (err) {
    next(err);
  }
}

app.use(jsonParser);

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(400).send('JSON 解析错误');
});

app.post('/data', (req, res) => {
  res.send('数据已接收');
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

jsonParser 中间件中,如果 JSON 解析失败,会捕获错误并通过 next(err) 将错误传递给错误处理中间件。错误处理中间件会记录错误堆栈信息并返回一个合适的错误响应。

中间件的组合与复用

Express 中间件的一个强大之处在于它们可以很容易地组合和复用。我们可以将多个中间件组合起来形成一个更复杂的功能,也可以在不同的应用或路由中复用相同的中间件。

例如,我们可以创建一个包含日志记录和身份验证的中间件组合:

const express = require('express');
const app = express();

function logger(req, res, next) {
  console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
  next();
}

// 模拟用户数据
const users = [
  { id: 1, token: 'validToken1' },
  { id: 2, token: 'validToken2' }
];

function authenticate(req, res, next) {
  const token = req.headers['authorization'];
  if (token) {
    const user = users.find(u => u.token === token);
    if (user) {
      next();
    } else {
      res.status(401).send('未授权');
    }
  } else {
    res.status(401).send('未授权');
  }
}

const combinedMiddleware = [logger, authenticate];

app.get('/protected', combinedMiddleware, (req, res) => {
  res.send('这是受保护的资源');
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

在上述代码中,combinedMiddleware 数组包含了 loggerauthenticate 两个中间件。通过将这个数组传递给路由,我们实现了日志记录和身份验证的组合功能。而且,如果其他路由也需要同样的功能,我们可以直接复用 combinedMiddleware

中间件与 Express 应用架构

合理使用中间件可以极大地优化 Express 应用的架构。通过将不同的功能模块化为中间件,我们可以实现代码的解耦和可维护性。

例如,在一个大型的 Express 应用中,我们可以将日志记录、身份验证、数据验证、错误处理等功能分别封装成中间件。这样,在路由定义时,我们可以根据需要灵活地组合这些中间件。

假设我们有一个电商应用,其中商品列表页面可能只需要日志记录中间件,而用户下单页面则需要身份验证、数据验证和日志记录中间件。通过这种方式,我们可以根据不同的业务需求,以一种清晰、可维护的方式构建应用。

中间件在 Express 性能优化中的作用

中间件在 Express 应用的性能优化方面也起着重要作用。例如,缓存中间件可以缓存经常访问的数据,减少数据库查询次数。压缩中间件可以减少网络传输的数据量,提高页面加载速度。

以缓存中间件为例,我们可以使用 express - cache - response 中间件。首先安装:

npm install express - cache - response

然后在应用中使用:

const express = require('express');
const cacheResponse = require('express - cache - response');
const app = express();

const cache = cacheResponse({
  statusCodes: [200],
  duration: 60 * 1000 // 缓存 1 分钟
});

app.get('/data', cache, (req, res) => {
  // 模拟数据库查询
  const data = { message: '从数据库获取的数据' };
  res.json(data);
});

const port = 3000;
app.listen(port, () => {
  console.log(`应用在端口 ${port} 上运行`);
});

在上述代码中,cacheResponse 中间件会缓存响应数据。在缓存有效期内,相同的请求将直接从缓存中获取数据,而不需要再次执行数据库查询操作,从而提高了应用的性能。

中间件与 Express 生态系统

Express 的中间件机制为其丰富的生态系统奠定了基础。众多第三方中间件的存在,使得开发者可以快速地为应用添加各种功能,而无需从头编写复杂的代码。

例如,multer 中间件用于处理文件上传,helmet 中间件用于增强应用的安全性,cookie - parser 中间件用于解析 cookie 数据等等。这些中间件与 Express 的中间件机制无缝集成,使得开发者可以轻松构建功能强大的 Web 应用。

同时,开发者也可以通过发布自己的中间件到 npm 仓库,为 Express 生态系统做出贡献,进一步推动 Node.js Web 开发的发展。

中间件在不同应用场景下的应用

  1. 单页应用(SPA):在 SPA 开发中,中间件可以用于处理 API 请求的身份验证、日志记录以及数据预处理等。例如,在 React 或 Vue 构建的 SPA 应用中,后端 Express 服务器使用中间件来确保只有授权用户可以访问 API 接口,同时记录 API 请求的相关信息。
  2. 多页应用(MPA):对于 MPA,中间件可以用于处理页面渲染过程中的数据获取、权限控制等。比如,在一个基于 Express 的 MPA 应用中,中间件可以在页面渲染之前检查用户是否有权限访问该页面,并从数据库中获取必要的数据传递给视图引擎。
  3. 微服务架构:在微服务架构中,Express 中间件可以用于每个微服务内部的请求处理。例如,日志记录中间件可以记录每个微服务的请求信息,方便进行故障排查;身份验证中间件可以确保只有授权的微服务之间可以进行通信。

通过以上对 Express 中间件机制的详细解析以及各种实践示例,相信你已经对 Express 中间件有了更深入的理解。在实际开发中,合理运用中间件可以让你的 Express 应用更加健壮、高效且易于维护。无论是小型项目还是大型企业级应用,中间件都将是你构建强大 Node.js 应用的有力工具。