如何实现一个自定义的中间件

178 阅读5分钟

在开发 Express 应用时,常常需要处理客户端发来的 JSON 数据。express.json() 是一个内置的中间件,它能够自动解析请求体中的 JSON 数据,并将其附加到 req.body 上,供后续路由使用。然而,作为一个开发者,你可能有兴趣了解如何自己实现类似的中间件。这不仅能加深你对 Express 工作原理的理解,还能帮助你在特定场景下进行更细致的定制。

在这篇文章中,我们将实现一个类似于 express.json() 的自定义中间件,它能够解析请求体中的 JSON 数据,并将解析结果存储到 req.body 中,供后续的中间件或路由使用。

目标

我们将实现一个中间件,它具备以下功能:

  • 解析传入的 JSON 请求体。
  • 如果请求体是无效的 JSON,返回 400 错误。
  • 将解析后的数据存储在 req.body 中,供后续使用。
  • 如果请求的 Content-Type 不是 application/json,则跳过此中间件,传递控制权给下一个中间件或路由。
  "scripts": {
    "dev": "nodemon --watch ./src ./src/main.js"
  },
   "dependencies": {
    "express": "^4.21.1",
  },
  "devDependencies": {
    "nodemon": "^2.0.22"
  }

1. 认识 express.json()

在开始实现之前,我们先了解一下 Express 中内置的 express.json() 中间件是如何工作的。express.json() 负责解析客户端发送的 JSON 格式的请求体,并将解析后的结果存储在 req.body 中。它的工作流程如下:

  1. 判断请求的 Content-Type 是否为 application/json
  2. 读取请求体的数据
  3. 使用 JSON.parse() 解析请求体
  4. 如果解析成功,将结果附加到 req.body
  5. 如果解析失败,返回 400 错误

我们将实现一个类似的中间件。

2. 编写自定义的 jsonParser 中间件

我们来一步步实现这个中间件。

2.1 代码实现

--| ./src/middleware/index.js

function jsonParser(req, res, next) {

  if (req.headers['content-type'] !== 'application/json') {
    return next();  // 如果不是 JSON 请求,跳过此中间件
  }

  let data = '';

  // 监听 data 事件,接收数据
  req.on('data', chunk => {
    data += chunk;  // 拼接接收到的数据块
  });

  // 当请求体接收完时,进行 JSON 解析
  req.on('end', () => {
    try {
      // 解析 JSON 数据
      req.body = JSON.parse(data);
      next();  // 继续传递控制权到下一个中间件或路由
    } catch (err) {
      // 如果 JSON 解析失败,返回 400 错误响应
      res.statusCode = 400;
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify({ error: 'Invalid JSON' }));
    }
  });

  // 监听请求错误
  req.on('error', (err) => {
    res.statusCode = 500;
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ error: 'Server Error' }));
  });
}

module.exports = jsonParser;
/**
 *  @description 请求单位时间内限制中间件
 * */ 
let rateLimit = new Map()
function requestRateLimiter(req, res, next) {
    const ip = req.ip;
    const currentTime = Date.now();
    if (!rateLimit.has(ip)) {
        rateLimit.set(ip, {count: 1, timestamp: currentTime })
        return next()
    }
    const data = rateLimit.get(ip)
    if (currentTime - data.timestamp < 6000) {
        if (data.count >= 5) {
            return res.json({code: 500, msg: '请求次数在6s内大于5次,请稍后再试'})
        }
        data.count++
    }else {
        rateLimit.set(ip,{count:1, timestamp: currentTime})
    }
    next()
}
module.exports = {
    jsonParser,
    requestRateLimiter
};

3. 使用自定义的 jsonParser 中间件

接下来,我们将展示如何在 Express 应用中使用这个自定义的 jsonParser 中间件。

3.1 设置 Express 应用

--| ./src/main.js

const express = require('express');
const jsonParser = require('./middleware'); // 引入我们自定义的中间件
const app = express();

// 使用自定义的 JSON 解析中间件
app.use(jsonParser);

/**
 * @description 创建新的item
 */
app.post('/addItem', (req, res) => {
    console.log(req.body)
  const { name, description } = req.body;
  if (!name || !description) {
    return sendErrorResponse(res, { code: 501, msg: '姓名或者描述不能为空'})
  }
  const newObj = {
    id: list.length + 1,
    name,
    description  
  }
  list.push(newObj)
  sendResponse(res, { code: 200, msg: 'success'})
//   res.statusCode(200).json(newObj)
})

// 启动应用
app.listen(3000, () => {
  console.log('服务运行在localhost-->3000端口');
});

3.2 代码解释

  • 我们引入了自定义的 jsonParser 中间件,并通过 app.use(jsonParser) 将它应用到所有请求中。
  • /addItem 路由中,我们可以通过 req.body 访问到客户端发送的 JSON 数据,进行后续的处理。

4. 认识路径参数和查询参数

特性路径参数查询参数
位置URL 路径的一部分URL 路径之后,通过 ? 分隔
语义描述资源的唯一标识符描述附加信息、筛选条件、分页、排序等
是否可选必须提供(通常是必需的)可选,客户端可以根据需求选择性地传递
格式通常没有键值对,仅表示路径的一部分键值对形式,多个查询参数通过 & 连接
常见用途标识单个资源或资源的子集用于筛选、排序、分页、过滤等附加信息的传递
示例/users/123(获取 userId123 的用户)/products?category=electronics&page=2&sort=asc(获取电子类商品,第二页,按升序排序)

4.1 何时使用路径参数,何时使用查询参数

  • 使用路径参数

    • 当 URL 需要表示某个资源或资源的子集时,使用路径参数。
    • 路径参数通常是必需的,用于标识特定的资源。
    • 适用于 层级结构 的资源,比如 /users/:userId/posts/:postId/comments/:commentId
  • 使用查询参数

    • 当需要附加额外的信息、筛选条件、分页或排序时,使用查询参数。
    • 查询参数通常是可选的,用于 控制响应的格式或内容,而不是标识资源。
    • 适用于 过滤、排序、分页等功能,比如 /search?query=apple&page=2&sort=desc

5. 总结

在这篇文章中,我们实现了一个类似于 express.json() 的自定义中间件,具备以下功能:

  • 解析 JSON 格式的请求体。
  • 将解析后的数据存储在 req.body 中,供后续的中间件或路由使用。
  • 错误处理:如果 JSON 格式不正确,返回 400 错误;如果出现其他服务器错误,返回 500 错误。

这个中间件的实现过程加深了我们对 Express 中间件和 HTTP 请求处理机制的理解。