為什么我們需要body-parser


body-parser代碼邏輯
-
data事件:當request接收到數據的時候觸發,在數據傳輸結束前可能會觸發多次,在事件回調里可以接收到Buffer類型的數據參數,我們可以將Buffer數據對象收集到數組里
-
end事件:請求數據接收結束時候觸發,不提供參數,我們可以在這里將之前收集的Buffer數組集中處理,最后輸出將request.body輸出。
數據處理流程
- 在request的data事件觸發時候,收集Buffer對象,將其放到一個命名為chunks的數組中
- 在request的end事件觸發時,通過Buffer.concat(chunks)將Buffer數組整合成單一的大的Buffer對象
- 解析請求首部的Content-Encoding,根據類型,如gzip,deflate等調用相應的解壓縮函數如Zlib.gunzip,將2中得到的Buffer解壓,返回的是解壓后的Buffer對象
- 解析請求的charset字符編碼,根據其類型,如gbk或者utf-8,調用iconv庫提供的decode(buffer, charset)方法,根據字符編碼將3中的Buffer轉換成字符串
- 最后,根據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
上一篇文章
【完】