尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

nodejs-learning-guide 精读:Express 中间件 body-parser 的解析原理与手写实现 文档教程后端【免费下载链接】nodejs-learning-guideNodejs学习笔记以及经验总结公众号程序猿小卡项目地址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?nickchyingp // 输出为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一、1parser-json解析 application/json一、2parser-urlencoded解析 application/x-www-form-urlencoded一、3parser-with-encoding处理 gbk 等非默认编码二parser-with-gzip处理 gzip 压缩请求体三入门基础先看懂一个 POST 请求报文在正式讲解前我们先来看一个 POST 请求的报文POST /test HTTP/1.1 Host: 127.0.0.1:3000 Content-Type: text/plain; charsetutf8 Content-Encoding: gzip chyingp其中需要我们注意的有Content-Type、Content-Encoding以及报文主体Content-Type请求报文主体的类型、编码。常见的类型有text/plain、application/json、application/x-www-form-urlencoded常见的编码有utf8、gbk等。它的完整语法形如type/subtype; parametervalue编码通常以charset参数的形式跟在类型之后如text/plain; charsetutf8。Content-Encoding声明报文主体的压缩格式常见的取值有gzip、deflate、identity。identity表示不压缩这也是示例客户端中的默认取值。报文主体这里是个普通的文本字符串chyingp。对服务端而言解析请求体本质上就是回答三个问题请求体是什么格式由 Content-Type 决定、用什么编码由 Content-Type 的 charset 参数决定、是否被压缩过由 Content-Encoding 决定。body-parser 的全部工作就是围绕这三个头字段展开的。body-parser 主要做了什么body-parser实现的要点如下处理不同类型的请求体比如text、json、urlencoded等对应的报文主体的格式不同处理不同的编码比如utf8、gbk等处理不同的压缩类型比如gzip、deflate等其他边界、异常的处理。下文将逐条展开用最朴素的原生 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/plainvar 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/jsonvar 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对请求体进行格式化得到类似nickchyingp的字符串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-urlencodedbody-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()会把nickchyingp形式的字符串解析成{ 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有两个要点编码声明在Content-Type最后加上;charsetgbk请求体编码借助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; charsetgbk拆成{ 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要点如下压缩类型声明Content-Encoding赋值为gzip请求体压缩通过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模块对请求体进行解压缩操作gunzipvar 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.mdmultipart/form-data的完整处理方案、expresssession实现简易身份认证.md中间件组合实战、post-body.md依赖的技术模块iconv-litegbk 等非 UTF-8 编码的编解码、content-typeContent-Type 头解析、Node.js 内置zlibgzip/deflate 压缩流。赞分享文档教程后端【免费下载链接】nodejs-learning-guideNodejs学习笔记以及经验总结公众号程序猿小卡项目地址https://gitcode.com/gh_mirrors/no/nodejs-learning-guide点击查看免费下载相关推荐GPT-SoVITS 情感控制教程用参考音频给合成语音加上喜怒哀乐GPT SoVITS 情感控制教程用参考音频给合成语音加上喜怒哀乐 从一个具体场景说起 你在做有声书旁白要生气地说一句“别再跟着我了”。直接用普通 TTS文档教程后端Windows COM自动化PythonForWindows COM接口与WMI查询完全教程Windows COM自动化PythonForWindows COM接口与WMI查询完全教程 想在 Python 中实现 Windows COM 自动化 吗Express-validator中间件深度解析check、body、query的区别Express validator中间件深度解析check、body、query的区别 Express validator是Express.js生态中一个功能后端上一篇NBTExplorer终极指南免费开源Minecraft数据编辑神器下一篇NBTExplorer终极指南免费开源Minecraft数据编辑神器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表