【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輸出。

數據處理流程html

  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,identitynode

其中git

  • identity表示數據保持原樣,沒有通過壓縮
  • compress已經被大多數瀏覽器廢棄,Node沒有提供解壓的方法

因此咱們須要處理解壓的一共有三種數據類型github

  • gzip:採用zlib.gunzip方法解壓
  • deflate: 採用zlib.inflate方法解壓
  • br:採用zlib.brotliDecompress方法解壓

(注意!zlib.brotliDecompress方法在Node11.7以上版本纔會支持,並且不要看到名字裏有compress就誤覺得它是用來解壓compress壓縮的數據的,實際上它是用來處理br的)npm

代碼以下,咱們對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進行轉碼,代碼以下json

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-litepromise

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測網速

【完】

相關文章
相關標籤/搜索