news 2026/10/7 2:33:11

nodejs-learning-guide 精读:Express 中间件 body-parser 的解析原理与手写实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
nodejs-learning-guide 精读:Express 中间件 body-parser 的解析原理与手写实现
  • 文档
  • 教程
  • 后端

【免费下载链接】nodejs-learning-guide

Nodejs学习笔记以及经验总结,公众号"程序猿小卡"

项目地址:https://gitcode.com/gh_mirrors/no/nodejs-learning-guide
点击查看免费下载

body-parser是 Express 生态中最常用的请求体解析中间件,两行代码即可覆盖绝大多数 POST 请求的解析场景。本文以 nodejs-learning-guide 仓库中的 进阶/body-parser.md 为骨架,从 HTTP 报文出发,逐步拆解text/plain、application/json、application/x-www-form-urlencoded三类请求体的解析、gbk等非 UTF-8 编码的解码、gzip压缩流的解压,并给出可直接运行的客户端/服务端示例(源码位于 examples/2017.05.20-express-body-parser),帮助你理解其底层实现,读完即可独立实现一个极简版 body-parser。

写在前面:body-parser 是什么

body-parser是非常常用的 Express 中间件,作用是对 HTTP 请求体(request body)进行解析。它的使用非常简单,以下两行代码已经覆盖了大部分的使用场景:

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

以仓库中的 started/server.js 为例,一个最小的 Express 应用只需要挂载bodyParser.urlencoded()即可:

var express = require('express'); var bodyParser = require('body-parser'); var app = express(); app.use(bodyParser.urlencoded({extended: false})); app.get('/test', function (req, res, next) { // 访问地址为:http://127.0.0.1:3030/test?nick=chyingp // 输出为:nick is chyingp res.end(`nick is ${req.query.nick}`); }); app.listen(3030);

本文从简单例子出发,探究body-parser的内部实现,而不是重复官方使用文档。为了让读者能亲手验证每一个结论,仓库在 examples/2017.05.20-express-body-parser 下按主题拆分了六组可直接运行的服务端/客户端示例:

目录主题对应章节
startedbody-parser 基础使用开头
parser-text解析 text/plain一、1
parser-json解析 application/json一、2
parser-urlencoded解析 application/x-www-form-urlencoded一、3
parser-with-encoding处理 gbk 等非默认编码二
parser-with-gzip处理 gzip 压缩请求体三

入门基础:先看懂一个 POST 请求报文

在正式讲解前,我们先来看一个 POST 请求的报文:

POST /test HTTP/1.1 Host: 127.0.0.1:3000 Content-Type: text/plain; charset=utf8 Content-Encoding: gzip chyingp

其中需要我们注意的有Content-Type、Content-Encoding以及报文主体:

  • Content-Type:请求报文主体的类型、编码。常见的类型有text/plain、application/json、application/x-www-form-urlencoded;常见的编码有utf8、gbk等。它的完整语法形如type/subtype; parameter=value,编码通常以charset参数的形式跟在类型之后(如text/plain; charset=utf8)。
  • Content-Encoding:声明报文主体的压缩格式,常见的取值有gzip、deflate、identity。identity表示不压缩,这也是示例客户端中的默认取值。
  • 报文主体:这里是个普通的文本字符串chyingp。

对服务端而言,"解析请求体"本质上就是回答三个问题:请求体是什么格式(由 Content-Type 决定)、用什么编码(由 Content-Type 的 charset 参数决定)、是否被压缩过(由 Content-Encoding 决定)。body-parser 的全部工作,就是围绕这三个头字段展开的。

body-parser 主要做了什么

body-parser实现的要点如下:

  1. 处理不同类型的请求体:比如text、json、urlencoded等,对应的报文主体的格式不同;
  2. 处理不同的编码:比如utf8、gbk等;
  3. 处理不同的压缩类型:比如gzip、deflate等;
  4. 其他边界、异常的处理。

下文将逐条展开,用最朴素的原生 Node.js 代码复现每一种能力——你会发现,body-parser 的核心逻辑并不神秘,每一步都是可独立验证的。

一、处理不同类型请求体

为了方便读者测试,以下例子均包含服务端、客户端代码,完整代码见 examples/2017.05.20-express-body-parser 下的对应目录。运行方式为:先node server.js启动服务端(默认监听 3000 端口),再在另一个终端node client.js发起请求,服务端会把解析结果回显在客户端终端中。

解析 text/plain

客户端请求的代码如下(见 parser-text/client.js),采用默认编码,不对请求体进行压缩,请求体类型为text/plain:

var http = require('http'); var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'text/plain', 'Content-Encoding': 'identity' } }; var client = http.request(options, (res) => { res.pipe(process.stdout); }); client.end('chyingp');

服务端代码如下(见 parser-text/server.js)。text/plain类型处理比较简单,就是 Buffer 的拼接:

var http = require('http'); var parsePostBody = function (req, done) { var arr = []; var chunks; req.on('data', buff => { arr.push(buff); }); req.on('end', () => { chunks = Buffer.concat(arr); done(chunks); }); }; var server = http.createServer(function (req, res) { parsePostBody(req, (chunks) => { var body = chunks.toString(); res.end(`Your nick is ${body}`) }); }); server.listen(3000);

这里的实现思路值得记住:请求体可能被拆成多个 TCP 分片到达,因此不能直接读取一次data事件就拿结果,而是把每次收到的 Buffer 依次push进数组,等end事件触发后用Buffer.concat(arr)拼成完整请求体,最后toString()得到字符串。body-parser 内部对请求体的收集逻辑与这段代码一脉相承。

解析 application/json

客户端代码如下(见 parser-json/client.js),把Content-Type换成application/json:

var http = require('http'); var querystring = require('querystring'); var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Encoding': 'identity' } }; var jsonBody = { nick: 'chyingp' }; var client = http.request(options, (res) => { res.pipe(process.stdout); }); client.end( JSON.stringify(jsonBody) );

服务端代码如下(见 parser-json/server.js)。相比text/plain,只是多了个JSON.parse()的过程:

var http = require('http'); var parsePostBody = function (req, done) { var length = req.headers['content-length'] - 0; var arr = []; var chunks; req.on('data', buff => { arr.push(buff); }); req.on('end', () => { chunks = Buffer.concat(arr); done(chunks); }); }; var server = http.createServer(function (req, res) { parsePostBody(req, (chunks) => { var json = JSON.parse( chunks.toString() ); // 关键代码 res.end(`Your nick is ${json.nick}`) }); }); server.listen(3000);

注意两点:一是发送端用JSON.stringify(jsonBody)把对象序列化成 JSON 字符串;二是接收端用JSON.parse(chunks.toString())把文本还原成对象,随后就可以像访问普通对象一样取用json.nick。这对应 body-parser 中json解析器的核心动作。

解析 application/x-www-form-urlencoded

客户端代码如下(见 parser-urlencoded/client.js),这里通过querystring对请求体进行格式化,得到类似nick=chyingp的字符串:

var http = require('http'); var querystring = require('querystring'); var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'form/x-www-form-urlencoded', 'Content-Encoding': 'identity' } }; var postBody = { nick: 'chyingp' }; var client = http.request(options, (res) => { res.pipe(process.stdout); }); client.end( querystring.stringify(postBody) );

提示:示例中的Content-Type写成了form/x-www-form-urlencoded,这只是为了演示而随意起的类型名。实际生产环境中,浏览器提交表单的标准类型是application/x-www-form-urlencoded,body-parser 的urlencoded解析器也正是匹配后者。

服务端代码如下(见 parser-urlencoded/server.js),同样跟text/plain的解析差不多,就多了个querystring.parse()的调用:

var http = require('http'); var querystring = require('querystring'); var parsePostBody = function (req, done) { var length = req.headers['content-length'] - 0; var arr = []; var chunks; req.on('data', buff => { arr.push(buff); }); req.on('end', () => { chunks = Buffer.concat(arr); done(chunks); }); }; var server = http.createServer(function (req, res) { parsePostBody(req, (chunks) => { var body = querystring.parse( chunks.toString() ); // 关键代码 res.end(`Your nick is ${body.nick}`) }); }); server.listen(3000);

querystring.parse()会把nick=chyingp形式的字符串解析成{ nick: 'chyingp' }。这就是 HTML 表单(POST 方法)默认提交格式的解析原理。

小结:三种类型的差异只在一个转换函数

把三个服务端实现放在一起对比,可以看到收集请求体的骨架代码完全一致,差异仅在最后的"文本 → 结构"一步:

Content-Type转换函数产物
text/plainchunks.toString()字符串
application/jsonJSON.parse(chunks.toString())对象
application/x-www-form-urlencodedquerystring.parse(chunks.toString())对象

这正是 body-parser 的设计思路:先统一收集请求体 Buffer,再根据Content-Type分发到不同的解析器,由各解析器完成对应的转换。

二、处理不同编码

很多时候,来自客户端的请求采用的不一定是默认的utf8编码,这个时候就需要对请求体进行解码处理。

客户端请求如下(见 parser-with-encoding/client.js),有两个要点:

  1. 编码声明:在Content-Type最后加上;charset=gbk;
  2. 请求体编码:借助iconv-lite,对请求体进行编码iconv.encode('程序猿小卡', encoding)。
var http = require('http'); var iconv = require('iconv-lite'); var encoding = 'gbk'; // 请求编码 var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'text/plain; charset=' + encoding, 'Content-Encoding': 'identity', } }; // 备注:nodejs本身不支持gbk编码,所以请求发送前,需要先进行编码 var buff = iconv.encode('程序猿小卡', encoding); var client = http.request(options, (res) => { res.pipe(process.stdout); }); client.end(buff, encoding);

服务端代码如下(见 parser-with-encoding/server.js),这里多了两个步骤:编码判断、解码操作。首先通过Content-Type获取编码类型gbk,然后通过iconv-lite进行反向解码操作:

var http = require('http'); var contentType = require('content-type'); var iconv = require('iconv-lite'); var parsePostBody = function (req, done) { var obj = contentType.parse(req.headers['content-type']); var charset = obj.parameters.charset; // 编码判断:这里获取到的值是 'gbk' var arr = []; var chunks; req.on('data', buff => { arr.push(buff); }); req.on('end', () => { chunks = Buffer.concat(arr); var body = iconv.decode(chunks, charset); // 解码操作 done(body); }); }; var server = http.createServer(function (req, res) { parsePostBody(req, (body) => { res.end(`Your nick is ${body}`) }); }); server.listen(3000);

这里值得留意content-type这个模块:contentType.parse()会把text/plain; charset=gbk拆成{ type: 'text/plain', parameters: { charset: 'gbk' } }这样的结构,从而让我们能精确取到charset参数。body-parser 内部正是借助类似机制来判断请求体的编码,并在charset缺省时回退到默认编码(对text、json等类型通常是utf-8)。

另一个关键点是:Node.js 原生并不支持 gbk 编码,UTF-8 之外的中文编码(gbk、gb2312、big5 等)都需要借助iconv-lite这类第三方库完成编码/解码,这就是服务端必须iconv.decode(chunks, charset)的原因。

三、处理不同压缩类型

这里举一个gzip压缩的例子。客户端代码如下(见 parser-with-gzip/client.js),要点如下:

  1. 压缩类型声明:Content-Encoding赋值为gzip;
  2. 请求体压缩:通过zlib模块对请求体进行 gzip 压缩。
var http = require('http'); var zlib = require('zlib'); var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'text/plain', 'Content-Encoding': 'gzip' } }; var client = http.request(options, (res) => { res.pipe(process.stdout); }); // 注意:将 Content-Encoding 设置为 gzip 的同时,发送给服务端的数据也应该先进行gzip var buff = zlib.gzipSync('chyingp'); client.end(buff);

服务端代码如下(见 parser-with-gzip/server.js),通过zlib模块对请求体进行解压缩操作(gunzip):

var http = require('http'); var zlib = require('zlib'); var parsePostBody = function (req, done) { var length = req.headers['content-length'] - 0; var contentEncoding = req.headers['content-encoding']; var stream = req; // 关键代码如下 if(contentEncoding === 'gzip') { stream = zlib.createGunzip(); req.pipe(stream); } var arr = []; var chunks; stream.on('data', buff => { arr.push(buff); }); stream.on('end', () => { chunks = Buffer.concat(arr); done(chunks); }); stream.on('error', error => console.error(error.message)); }; var server = http.createServer(function (req, res) { parsePostBody(req, (chunks) => { var body = chunks.toString(); res.end(`Your nick is ${body}`) }); }); server.listen(3000);

这段代码展示了stream思维:req本身是一个 Readable 流。当检测到Content-Encoding: gzip时,用zlib.createGunzip()创建一个解压转换流,然后req.pipe(stream)让请求体数据流经解压流,再在stream上监听data/end收集解压后的 Buffer。这样"收集请求体"的逻辑只需写一遍,压缩与否只影响数据源是req还是解压后的stream。

从实现上看,body-parser 的处理方式与此一致:它会根据Content-Encoding创建对应的解压流(gzip、deflate等),把请求体接入解压管道后再进入具体的类型解析器。

写在后面:核心不复杂,复杂的是边界

body-parser的核心实现并不复杂,翻看源码后你会发现,更多的代码是在处理异常跟边界。所谓"边界",至少包括以下几类,本文的示例出于演示目的大多刻意避开了:

  • 请求体超过limit上限(body-parser 默认对请求体大小有限制,超限应返回 413 或抛出错误);
  • Content-Type与解析器不匹配(body-parser 靠type-is之类的匹配机制决定是否接管某个请求,不匹配就直接next()放行);
  • JSON.parse失败等解析异常(应返回 400 而非让服务端进程崩溃);
  • charset缺失或不被支持、压缩流解压失败(stream上的error事件需要被监听,正如 gzip 示例中所做的那样);
  • 空请求体、重复的content-type头等协议层面的脏数据。

另外,对于 POST 请求还有一个非常常见的Content-Type是multipart/form-data(主要用于文件上传),它的处理相对复杂,body-parser不打算对其进行支持,通常交给multer等专门中间件处理。

相关链接与延伸阅读

  • 本文配套的可运行示例代码:examples/2017.05.20-express-body-parser(内含started、parser-text、parser-json、parser-urlencoded、parser-with-encoding、parser-with-gzip六组客户端/服务端代码);
  • 正文所依据的原始笔记:进阶/body-parser.md;
  • 仓库中与请求体解析相关的延伸主题:文件上传-multer.md(multipart/form-data的完整处理方案)、express+session实现简易身份认证.md(中间件组合实战)、post-body.md;
  • 依赖的技术模块:iconv-lite(gbk 等非 UTF-8 编码的编解码)、content-type(Content-Type 头解析)、Node.js 内置zlib(gzip/deflate 压缩流)。
  • 文档
  • 教程
  • 后端

【免费下载链接】nodejs-learning-guide

Nodejs学习笔记以及经验总结,公众号"程序猿小卡"

项目地址:https://gitcode.com/gh_mirrors/no/nodejs-learning-guide
点击查看免费下载
上一篇:NBTExplorer终极指南:免费开源Minecraft数据编辑神器
下一篇:NBTExplorer终极指南:免费开源Minecraft数据编辑神器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:32:47

RK3588触摸屏开发到YOLOv8部署:嵌入式AI完整实战指南

做RK3588触摸屏开发这几年,我最大的感受是:网上资料多而杂,真正能一口气把从硬件点亮到AI应用跑通的完整链路讲清楚的内容,太少了。很多朋友板子买回来,第一步就卡在屏幕不亮、触摸没反应上,更别提后面还要…

作者头像 李华
网站建设 2026/10/7 2:32:31

文件学习:从杂乱无章到个人知识库的高效整理方法论

不知道你有没有这种经历:硬盘里攒了多年的文件,平时谁都想不起来,需要用的时候死活找不到,只能凭记忆一层层翻文件夹,最后实在不行重新做一份。更气人的是,刚整理完的桌面和文件夹,过两周又变得…

作者头像 李华
网站建设 2026/10/7 2:32:07

DeepEval LLM 评估:5 分钟跑通首次评估并接入 CI

DeepEval LLM 评估:5 分钟跑通首次评估并接入 CI 【免费下载链接】deepeval The LLM Evaluation Framework 项目地址: https://gitcode.com/GitHub_Trending/de/deepeval DeepEval 是一个开源的 LLM 评估框架,用"LLM 当法官"的方式给模…

作者头像 李华
网站建设 2026/10/7 2:31:56

扫地机器人视觉传感器选型指南:全局快门与vSLAM实战

1. 扫地机器人视觉方案选型背后的真实逻辑扫地机器人这个品类,从最早的随机碰撞式走到今天,核心分水岭就一个词:定位与建图。早期机器靠红外和碰撞传感器满地乱撞,效率低、覆盖率差,用户买回家最大的感受就是“这玩意儿…

作者头像 李华
网站建设 2026/10/7 2:31:21

Rethinking Verification for LLM Code Generation: From Generation to Testing

文章主要内容总结 研究背景:大型语言模型(LLMs)在代码生成基准测试(如HumanEval、LiveCodeBench)中表现显著,但现有评估套件的测试用例数量有限且同质化,导致细微错误未被检测,夸大了模型性能,影响强化学习中可验证奖励(RLVR)的准确性。 核心问题:现有测试用例存在…

作者头像 李华