• 【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测网速

    【完】

  • 相关阅读:
    JDK5.0新特性系列10.监控与管理虚拟机
    JDK5.0新特性系列7.使用ProcessBuilder执行本地命令
    JDK5.0新特性系列4.静态导入
    JDK5.0新特性系列8.泛型编程
    JDK5.0新特性系列11.2线程 任务执行架构
    JDK5.0新特性系列6.格式化输出
    Request的编码问题,客户端为繁体系统时为乱码解决了。
    智能手机之新手篇[转]
    一篇颇有感触的文章
    修复Oracle9i中DBA的密码
  • 原文地址:https://www.cnblogs.com/penghuwan/p/11374268.html
Copyright © 2020-2023  润新知