在开发 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 中。它的工作流程如下:
- 判断请求的
Content-Type是否为application/json。 - 读取请求体的数据。
- 使用
JSON.parse()解析请求体。 - 如果解析成功,将结果附加到
req.body。 - 如果解析失败,返回 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(获取 userId 为 123 的用户) | /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 请求处理机制的理解。