【Node.js】 bodyparser实现原理解析

为什么我们需要body-parser

也许你第一次和bodyparser相遇是在使用Koa框架的时候。当我们尝试从一个浏览器发来的POST请求中取得请求报文实体的时候,这个时候,我们想,这个从Koa自带的ctx.body里面取出来就可以了嘛!

唉!等等,但根据Koa文档,ctx.body等同于ctx.res.body,所以从ctx.body取出来的是空的响应报文,而不是请求报文的实体哦

于是这时候又打算从Node文档里找找request对象有没有可以提供查询请求报文的属性,结果自然是Node文档自然会告诉你结果——

所以,这个时候我们需要的是——

bodyparser是一类处理request的body的中间件函数,例如Koa-bodyparser就是和Koa框架搭配使用的中间件,帮助没有内置处理该功能的Koa框架提供解析request.body的方法,通过app.use加载Koa-bodyparser后,在Koa中就可以通过ctx.request.body访问到请求报文的报文实体啦!

body-parser代码逻辑

无论是Node的哪一款body-parser,其原理都是类似的今天我们就编写一个getRequestBody的函数,解析出request.body,以尽管中窥豹之理。

要编写body-parser的代码,首先要了解两个方面的逻辑:请求相关事件和数据处理流程

请求相关事件

  • data事件:当request接收到数据的时候触发,在数据传输结束前可能会触发多次,在事件回调里可以接收到Buffer类型的数据参数,我们可以将Buffer数据对象收集到数组里
  • end事件:请求数据接收结束时候触发,不提供参数,我们可以在这里将之前收集的Buffer数组集中处理,最后输出将request.body输出。

数据处理流程

  1. 在request的data事件触发时候,收集Buffer对象,将其放到一个命名为chunks的数组中
  2. 在request的end事件触发时,通过Buffer.concat(chunks)将Buffer数组整合成单一的大的Buffer对象
  3. 解析请求首部的Content-Encoding,根据类型,如gzip,deflate等调用相应的解压缩函数如Zlib.gunzip,将2中得到的Buffer解压,返回的是解压后的Buffer对象
  4. 解析请求的charset字符编码,根据其类型,如gbk或者utf-8,调用iconv库提供的decode(buffer, charset)方法,根据字符编码将3中的Buffer转换成字符串
  5. 最后,根据Content-Type,如application/json或‘application/x-www-form-urlencoded‘对4中得到的字符串做相应的解析处理,得到最后的对象,作为request.body返回

下面展示下相关的代码

整体代码结构

// 根据Content-Encoding判断是否解压,如需则调用相应解压函数
async function transformEncode(buffer, encode) {
   // ...
}
// charset转码
function transformCharset(buffer, charset) {
  // ...
}

// 根据content-type做最后的数据格式化
function formatData(str, contentType) {
  // ...
}

// 返回Promise
function getRequestBody(req, res) {
    return new Promise(async (resolve, reject) => {
        const chunks = [];
        req.on(‘data‘, buf => {
            chunks.push(buf);
        })
        req.on(‘end‘, async () => {
            let buffer = Buffer.concat(chunks);
            // 获取content-encoding
            const encode = req.headers[‘content-encoding‘];
            // 获取content-type
            const { type, parameters } = contentType.parse(req);
            // 获取charset
            const charset = parameters.charset;
            // 解压缩
            buffer = await transformEncode(buffer, encode);
            // 转换字符编码
            const str = transformCharset(buffer, charset);
            // 根据类型输出不同格式的数据,如字符串或JSON对象
            const result = formatData(str, type);
            resolve(result);
        })
    }).catch(err => { throw err; })
}

Step0.Promise的编程风格

function getRequestBody(req, res) {
    return new Promise(async (resolve, reject) => {
      // ...
    }
}

Step1.data事件的处理

const chunks = [];
req.on(‘data‘, buf => {
  chunks.push(buf);
})

Step2.end事件的处理

const contentType = require(‘content-type‘);
const iconv = require(‘iconv-lite‘);

req.on(‘end‘, async () => {
 let buffer = Buffer.concat(chunks);
 // 获取content-encoding
 const encode = req.headers[‘content-encoding‘];
 // 获取content-type
 const { type, parameters } = contentType.parse(req);
 // 获取charset
 const charset = parameters.charset;
 // 解压缩
 buffer = await transformEncode(buffer, encode);
 // 转换字符编码
 const str = transformCharset(buffer, charset);
 // 根据类型输出不同格式的数据,如字符串或JSON对象
 const result = formatData(str, type);
  resolve(result);
}

Step3.根据Content-Encoding进行解压处理

Content-Encoding可分为四种值:gzip,compress,deflate,br,identity

其中

  • identity表示数据保持原样,没有经过压缩
  • compress已经被大多数浏览器废弃,Node没有提供解压的方法

所以我们需要处理解压的一共有三种数据类型

  • gzip:采用zlib.gunzip方法解压
  • deflate: 采用zlib.inflate方法解压
  • br:采用zlib.brotliDecompress方法解压

(注意!zlib.brotliDecompress方法在Node11.7以上版本才会支持,而且不要看到名字里有compress就误以为它是用来解压compress压缩的数据的,实际上它是用来处理br的)

代码如下,我们对zlib.gunzip等回调类方法通过promisify转成Promise编码风格

const promisify = util.promisify;
// node 11.7版本以上才支持此方法
const brotliDecompress = zlib.brotliDecompress && promisify(zlib.brotliDecompress);

const gunzip = promisify(zlib.gunzip);
const inflate = promisify(zlib.inflate);

const querystring = require(‘querystring‘);

// 根据Content-Encoding判断是否解压,如需则调用相应解压函数
async function transformEncode(buffer, encode) {
    let resultBuf = null;
    debugger;
    switch (encode) {
        case ‘br‘:
            if (!brotliDecompress) {
                throw new Error(‘Node版本过低! 11.6版本以上才支持brotliDecompress方法‘)
            }
            resultBuf = await brotliDecompress(buffer);
            break;
        case ‘gzip‘:
            resultBuf = await gunzip(buffer);
            break;
        case ‘deflate‘:
            resultBuf = await inflate(buffer);
            break;
        default:
            resultBuf = buffer;
            break;
    }
    return resultBuf;
}

Step4.根据charset进行转码处理

我们采用iconv-lite对charset进行转码,代码如下

const iconv = require(‘iconv-lite‘);
// charset转码
function transformCharset(buffer, charset) {
    charset = charset || ‘UTF-8‘;
    // iconv将Buffer转化为对应charset编码的String
    const result = iconv.decode(buffer, charset);
    return result;
}

来!传送门

https://link.zhihu.com/?target=https%3A//www.npmjs.com/package/iconv-lite

Step5.根据contentType将4中得到的字符串数据进行格式化

具体的处理方式分三种情况:

  • 对text/plain 保持原样,不做处理,仍然是字符串
  • 对application/x-www-form-urlencoded,得到的是类似于key1=val1&key2=val2的数据,通过querystring模块的parse方法转成{ key:val }结构的对象
  • 对于application/json,通过JSON.parse(str)一波带走

代码如下

const querystring = require(‘querystring‘);
// 根据content-type做最后的数据格式化
function formatData(str, contentType) {
    let result = ‘‘;
    switch (contentType) {
        case ‘text/plain‘:
            result = str;
            break;
        case ‘application/json‘:
            result = JSON.parse(str);
            break;
        case ‘application/x-www-form-urlencoded‘:
            result = querystring.parse(str);
            break;
        default:
            break;
    }
    return result;
}

测试代码

服务端

下面的代码你肯定知道要放在哪里了

// 省略其他代码
if (pathname === ‘/post‘) {
  // 调用getRequestBody,通过await修饰等待结果返回
  const body = await getRequestBody(req, res);
  console.log(body);
  return;
 }

前端采用fetch进行测试

在下面的代码中,我们连续三次发出不同的POST请求,携带不同类型的body数据,看看服务端会输出什么

var iconv = require(‘iconv-lite‘);
var querystring = require(‘querystring‘);
var gbkBody = {
    data: "我是彭湖湾",
    contentType: ‘application/json‘,
    charset: ‘gbk‘
};
// 转化为JSON数据
var gbkJson = JSON.stringify(gbkBody);
// 转为gbk编码
var gbkData = iconv.encode(gbkJson, "gbk");

var isoData = iconv.encode("我是彭湖湾,这句话采用UTF-8格式编码,content-type为text/plain", "UTF-8")

// 测试内容类型为application/json和charset=gbk的情况
fetch(‘/post‘, {
    method: ‘POST‘,
    headers: {
        "Content-Type": ‘application/json; charset=gbk‘
    },
    body: gbkData
});

// 测试内容类型为application/x-www-form-urlencoded和charset=UTF-8的情况
fetch(‘/post‘, {
    method: ‘POST‘,
    headers: {
        "Content-Type": ‘application/x-www-form-urlencoded; charset=UTF-8‘
    },
    body: querystring.stringify({
        data: "我是彭湖湾",
        contentType: ‘application/x-www-form-urlencoded‘,
        charset: ‘UTF-8‘
    })
});

// 测试内容类型为text/plain的情况
fetch(‘/post‘, {
    method: ‘POST‘,
    headers: {
        "Content-Type": ‘text/plain; charset=UTF-8‘
    },
    body: isoData
});

服务端输出结果

{
  data: ‘我是彭湖湾‘,
  contentType: ‘application/json‘,
  charset: ‘gbk‘
 }
 {
  data: ‘我是彭湖湾‘,
  contentType: ‘application/x-www-form-urlencoded‘,
  charset: ‘UTF-8‘
  }
  我是彭湖湾,这句话采用UTF-8格式编码,content-type为text/plain

问题和后记

Q1.为什么要对charset进行处理

其实本质上来说,charset前端一般都是固定为utf-8的, 甚至在JQuery的AJAX请求中,前端请求charset甚至是不可更改,只能是charset,但是在使用fetch等API的时候,的确是可以更改charset的,这个工作尝试满足一些比较偏僻的更改charset需求。

Q2:为什么要对content-encoding做处理呢?

一般情况下我们认为,考虑到前端发的AJAX之类的请求的数据量,是不需要做Gzip压缩的。但是向服务器发起请求的不一定只有前端,还可能是Node的客户端。这些Node客户端可能会向Node服务端传送压缩过后的数据流。 例如下面的代码所示

const zlib = require(‘zlib‘);
const request = require(‘request‘);
const data = zlib.gzipSync(Buffer.from("我是一个被Gzip压缩后的数据"));
request({
    method: ‘POST‘,
    url: ‘http://127.0.0.1:3000/post‘,
    headers: {//设置请求头
        "Content-Type": "text/plain",
        "Content-Encoding": "gzip"
    },
    body: data
})

项目的github和npm地址

https://github.com/penghuwan/body-parser-promise

https://www.npmjs.com/package/body-parser-promise

参考资料

Koa-bodyparser https://github.com/koajs/bodyparser

上一篇文章

如何用JavaScript测网速

【完】

原文地址:https://www.cnblogs.com/penghuwan/p/11374268.html

时间: 2024-10-28 03:25:11

【Node.js】 bodyparser实现原理解析的相关文章

node.js基本工作原理及流程

概述 Node.js是什么 Node 是一个服务器端 JavaScript 解释器,用于方便地搭建响应速度快.易于扩展的网络应用.Node.js 使用事件驱动, 非阻塞I/O 模型而得以轻量和高效,非常适合在分布式设备上运行数据密集型的实时应用. Node.js 是一个可以让 JavaScript 运行在浏览器之外的平台.它实现了诸如文件系统.模块.包.操作系统 API.网络通信等 Core JavaScript 没有或者不完善的功能.历史上将 JavaScript移植到浏览器外的计划不止一个,

91JS原生:node.js对URL的解析规则

用node.js搭建的服务器环境,其内置模块URL可以把浏览器传过来的URL进行解析,并根据解析结果做出相应的响应.1.引入node内置模块url,用url模块的parse方法来解析winUrl```javascriptvar url = require('url');var winUrl = url.parse('http://www.zhu.cn:80/ccc/index.html?name=zxt&age=26#33', true);console.log(winUrl); winUrl{

Node.js机制及原理理解初步

一.node.js优缺点 node.js是单线程. 好处就是 1)简单 2)高性能,避免了频繁的线程切换开销 3)占用资源小,因为是单线程,在大负荷情况下,对内存占用仍然很低 3)线程安全,没有加锁.解锁.死锁这些问题 php node.js 坏处就是 如何解决高并发? node使用异步IO和事件驱动(回调函数)来解决这个问题. 一般来说,高并发解决方案会提供多线程模型,为每个业务逻辑提供一个线程,通过系统线程切换来来弥补同步I/O调用的时间开销.像apache,是一个请求一个线程. 而node

Node.js机制及原理理解初步【转】

一.node.js优缺点 node.js是单线程. 好处就是 1)简单 2)高性能,避免了频繁的线程切换开销 3)占用资源小,因为是单线程,在大负荷情况下,对内存占用仍然很低 3)线程安全,没有加锁.解锁.死锁这些问题 php node.js 坏处就是 如何解决高并发? node使用异步IO和事件驱动(回调函数)来解决这个问题. 一般来说,高并发解决方案会提供多线程模型,为每个业务逻辑提供一个线程,通过系统线程切换来来弥补同步I/O调用的时间开销.像apache,是一个请求一个线程. 而node

vue.js响应式原理解析与实现

从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染.之后,再接触了vue.js,当时也一度很好奇vue.js是如何监测数据更新并且重新渲染页面.今天,就我们就来一步步解析vue.js响应式的原理,并且来实现一个简单的demo. 首先,先让我们来了解一些基础知识. 基础知识 Object.defineProperty es5新增了Object.defineProperty这个api,它可以允许我们为对象的属性来设定gette

vue.js响应式原理解析与实现—实现v-model与{{}}指令

离线浏览器软件    服务器远程连接 1.可多站同时下载.多站同时扒 2.可单页扒 3.可自定义, 重写JS\图片\CSS路径 4.执行全站下载后,会下载到本程序根目录下的html文件夹下. 5.全站替换,根据关键词替换.N个(多个)关键词替换.6.多线程下载7.自动补齐404不存在的链接页面(下载大站的时候需要)8.插入自定义的JS(全站插入)9.整站下载10.单页下载 此程序可以做到: 1,快速多线程下载整站,无论任何后缀(动态或者静态). 2,全站无错下载,包含CSS\JS\图片等等. 上

浅谈Node.js单线程模型

Node.js采用 事件驱动 和 异步I/O 的方式,实现了一个单线程.高并发的运行时环境,而单线程就意味着同一时间只能做一件事,那么Node.js如何利用单线程来实现高并发和异步I/O?本文将围绕这个问题来探讨Node.js的单线程模型: 1.高并发 一般来说,高并发的解决方案就是多线程模型,服务器为每个客户端请求分配一个线程,使用同步I/O,系统通过线程切换来弥补同步I/O调用的时间开销,比如Apache就是这种策略,由于I/O一般都是耗时操作,因此这种策略很难实现高性能,但非常简单,可以实

node.js 下依赖Express 实现post 4种方式提交参数

上面这个图好有意思啊,哈哈, v8威武啊.... 在2014年的最后一天和大家分享关于node.js 如何提交4种格式的post数据. 上上一篇说到了关于http协议里定义的4种常见数据的post方法 ,详细介绍请点击查看. 分别是这四种: www-form-urlencoded, form-data, application/json, text/xml Express 依赖 bodyParser 对请求的包体进行解析,默认支持:application/json, application/x-

[转载]Node入门 » 一本全面的Node.js教程

http://www.nodebeginner.org/index-zh-cn.html 作者: Manuel Kiessling 翻译: goddyzhao & GrayZhang & MondayChen 关于 本书致力于教会你如何用Node.js来开发应用,过程中会传授你所有所需的“高级”JavaScript知识.本书绝不是一本“Hello World”的教程. 状态 你正在阅读的已经是本书的最终版.因此,只有当进行错误更正以及针对新版本Node.js的改动进行对应的修正时,才会进行