深入koa-bodyparser原理解析
一、前置知識
在理解koa-bodyparser原理之前,首先需要了解部分HTTP相關(guān)的知識。
1、報(bào)文主體
HTTP報(bào)文主要分為請求報(bào)文和響應(yīng)報(bào)文,koa-bodyparser主要針對請求報(bào)文的處理。
請求報(bào)文主要由以下三個(gè)部分組成:
- 報(bào)文頭部
- 空行
- 報(bào)文主體
而koa-bodyparser中的body指的就是請求報(bào)文中的報(bào)文主體部分。
2、服務(wù)器端獲取報(bào)文主體流程
HTTP底層采用TCP提供可靠的字節(jié)流服務(wù),簡單而言就是報(bào)文主體部分會被轉(zhuǎn)化為二進(jìn)制數(shù)據(jù)在網(wǎng)絡(luò)中傳輸,所以服務(wù)器端首先需要拿到二進(jìn)制流數(shù)據(jù)。
談到網(wǎng)絡(luò)傳輸,當(dāng)然會涉及到傳輸速度的優(yōu)化,而其中一種優(yōu)化方式就是對內(nèi)容進(jìn)行壓縮編碼,常用的壓縮編碼方式有:
- gzip
- compress
- deflate
- identity(不執(zhí)行壓縮或不會變化的默認(rèn)編碼格式)
服務(wù)器端會根據(jù)報(bào)文頭部信息中的Content-Encoding確認(rèn)采用何種解壓編碼。
接下來就需要將二進(jìn)制數(shù)據(jù)轉(zhuǎn)換為相應(yīng)的字符,而字符也有不同的字符編碼方式,例如對于中文字符處理差異巨大的UTF-8和GBK,UTF-8編碼漢字通常需要三個(gè)字節(jié),而GBK只需要兩個(gè)字節(jié)。所以還需要在請求報(bào)文的頭部信息中設(shè)置Content-Type使用的字符編碼信息(默認(rèn)情況下采用的是UTF-8),這樣服務(wù)器端就可以利用相應(yīng)的字符規(guī)則進(jìn)行解碼,得到正確的字符串。
拿到字符串之后,服務(wù)器端又要問了:客戶端,你這一段字符串是啥意思???
根據(jù)不同的應(yīng)用場景,客戶端會對字符串采用不同的編碼方式,常見的編碼方式有:
- URL編碼方式: a=1&b=2
- JSON編碼方式: {a:1,b:2}
客戶端會將采用的字符串編碼方式設(shè)置在請求報(bào)文頭部信息的Content-Type屬性中,這樣服務(wù)器端根據(jù)相應(yīng)的字符串編碼規(guī)則進(jìn)行解碼,就能夠明白客戶端所傳遞的信息了。
下面一步步分析koa-bodyparser是如何處理這一系列操作,從而得到報(bào)文主體內(nèi)容。
二、獲取二進(jìn)制數(shù)據(jù)流
NodeJS中獲取請求報(bào)文主體二進(jìn)制數(shù)據(jù)流主要通過監(jiān)聽request對象的data事件完成:
// 示例一
const http = require('http')
http.createServer((req, res) => {
const body = []
req.on('data', chunk => {
body.push(chunk)
})
req.on('end', () => {
const chunks = Buffer.concat(body) // 接收到的二進(jìn)制數(shù)據(jù)流
// 利用res.end進(jìn)行響應(yīng)處理
res.end(chunks.toString())
})
}).listen(1234)
而koa-bodyparser主要是對co-body 的封裝,而【co-body】中主要是采用raw-body 模塊獲取請求報(bào)文主體的二進(jìn)制數(shù)據(jù)流,【row-body】主要是對上述示例代碼的封裝和健壯性處理。
三、內(nèi)容解碼
客戶端會將內(nèi)容編碼的方式放入請求報(bào)文頭部信息Content-Encoding屬性中,服務(wù)器端接收報(bào)文主體的二進(jìn)制數(shù)據(jù)了時(shí),會根據(jù)該頭部信息進(jìn)行解壓操作,當(dāng)然服務(wù)器端可以在響應(yīng)報(bào)文頭部信息Accept-Encoding屬性中添加支持的解壓方式。
而【row-body】主要采用 inflation 模塊進(jìn)行解壓處理。
四、字符解碼
一般而言,UTF-8是互聯(lián)網(wǎng)中主流的字符編碼方式,前面也提到了還有GBK編碼方式,相比較UTF-8,它編碼中文只需要2個(gè)字節(jié),那么在字符解碼時(shí)誤用UTF-8解碼GBK編碼的字符,就會出現(xiàn)中文亂碼的問題。
NodeJS主要通過Buffer處理二進(jìn)制數(shù)據(jù)流,但是它并不支持GBK字符編碼方式,需要通過 iconv-lite 模塊進(jìn)行處理。
【示例一】中的代碼就存在沒有正確處理字符編碼的問題,那么報(bào)文主體中的字符采用GBK編碼方式,必然會出現(xiàn)中文亂碼:
const request = require('request')
const iconv = require('iconv-lite')
request.post({
url: 'http://localhost:1234/',
body: iconv.encode('中文', 'gbk'),
headers: {
'Content-Type': 'text/plain;charset=GBK'
}
}, (error, response, body) => {
console.log(body) // 發(fā)生中文亂碼情況
})
NodeJS中的Buffer默認(rèn)是采用UTF-8字符編碼處理,這里借助【iconv-lite】模塊處理不同的字符編碼方式:
const chunks = Buffer.concat(body) res.end(iconv.decode(chunks, charset)) // charset通過Content-Type得到
五、字符串解碼
前面已經(jīng)提到了字符串的二種編碼方式,它們對應(yīng)的Content-Type分別為:
- URL編碼 application/x-www-form-urlencoded
- JSON編碼 application/json
對于前端來說,URL編碼并不陌生,經(jīng)常會用于URL拼接操作,唯一需要注意的是不要忘記對鍵值對進(jìn)行decodeURIComponent()處理。
當(dāng)客戶端發(fā)送請求主體時(shí),需要進(jìn)行編碼操作:
'a=1&b=2&c=3'
服務(wù)器端再根據(jù)URL編碼規(guī)則解碼,得到相應(yīng)的對象。
// URL編碼方式 簡單的解碼方法實(shí)現(xiàn)
function decode (qs, sep = '&', eq = '=') {
const obj = {}
qs = qs.split(sep)
for (let i = 0, max = qs.length; i < max; i++) {
const item = qs[i]
const index = item.indexOf(eq)
let key, value
if (~index) {
key = item.substr(0, index)
value = item.substr(index + 1)
} else {
key = item
value = ''
}
key = decodeURIComponent(key)
value = decodeURIComponent(value)
if (!obj.hasOwnProperty(key)) {
obj[key] = value
}
}
return obj
}
console.log(decode('a=1&b=2&c=3')) // { a: '1', b: '2', c: '3' }
URL編碼方式適合處理簡單的鍵值對數(shù)據(jù),并且很多框架的Ajax中的Content-Type默認(rèn)值都是它,但是對于復(fù)雜的嵌套對象就不太好處理了,這時(shí)就需要JSON編碼方式大顯身手了。
客戶端發(fā)送請求主體時(shí),只需要采用JSON.stringify進(jìn)行編碼。服務(wù)器端只需要采用JSON.parse進(jìn)行解碼即可:
const strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/;
function parse(str) {
if (!strict) return str ? JSON.parse(str) : str;
// 嚴(yán)格模式下,總是返回一個(gè)對象
if (!str) return {};
// 是否為合法的JSON字符串
if (!strictJSONReg.test(str)) {
throw new Error('invalid JSON, only supports object and array');
}
return JSON.parse(str);
}
除了上述兩種字符串編碼方式,koa-bodyparser還支持不采用任何字符串編碼方式的普通字符串。
三種字符串編碼的處理方式由【co-body】模塊提供,koa-bodyparser中通過判斷當(dāng)前Content-Type類型,調(diào)用不同的處理方式,將獲取到的結(jié)果掛載在ctx.request.body:
return async function bodyParser(ctx, next) {
if (ctx.request.body !== undefined) return await next();
if (ctx.disableBodyParser) return await next();
try {
// 最重要的一步 將解析的內(nèi)容掛載到koa的上下文中
const res = await parseBody(ctx);
ctx.request.body = 'parsed' in res ? res.parsed : {};
if (ctx.request.rawBody === undefined) ctx.request.rawBody = res.raw; // 保存原始字符串
} catch (err) {
if (onerror) {
onerror(err, ctx);
} else {
throw err;
}
}
await next();
};
async function parseBody(ctx) {
if (enableJson && ((detectJSON && detectJSON(ctx)) || ctx.request.is(jsonTypes))) {
return await parse.json(ctx, jsonOpts); // application/json等json type
}
if (enableForm && ctx.request.is(formTypes)) {
return await parse.form(ctx, formOpts); // application/x-www-form-urlencoded
}
if (enableText && ctx.request.is(textTypes)) {
return await parse.text(ctx, textOpts) || ''; // text/plain
}
return {};
}
};
其實(shí)還有一種比較常見的Content-type,當(dāng)采用表單上傳時(shí),報(bào)文主體中會包含多個(gè)實(shí)體主體:
------WebKitFormBoundaryqsAGMB6Us6F7s3SF Content-Disposition: form-data; name="image"; filename="image.png" Content-Type: image/png ------WebKitFormBoundaryqsAGMB6Us6F7s3SF Content-Disposition: form-data; name="text" ------WebKitFormBoundaryqsAGMB6Us6F7s3SF--
這種方式處理相對比較復(fù)雜,koa-bodyparser中并沒有提供該Content-Type的解析。(下一篇中應(yīng)該會介紹^_^)
五、總結(jié)
以上便是koa-bodyparser的核心實(shí)現(xiàn)原理,其中涉及到很多關(guān)于HTTP的基礎(chǔ)知識,對于HTTP不太熟悉的同學(xué),可以推薦看一波入門級寶典【圖解HTTP】。
最后留圖一張:
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
nvm報(bào)錯(cuò)Now?using?node?v版本號(64-bit)圖文解決方法
這篇文章主要給大家介紹了關(guān)于nvm報(bào)錯(cuò)Now?using?node?v版本號(64-bit)的解決方法,文中將解決的辦法介紹的非常詳細(xì),對遇到這個(gè)問題的朋友具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-11-11
node.js中的querystring.stringify方法使用說明
這篇文章主要介紹了node.js中的querystring.stringify方法使用說明,本文介紹了querystring.stringify的方法說明、語法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12
NodeJS實(shí)現(xiàn)不可逆加密與密碼密文保存的方法
這篇文章主要介紹了NodeJS實(shí)現(xiàn)不可逆加密與密碼密文保存的方法,簡單講述了不可逆加密與密碼密文保存的原理并結(jié)合實(shí)例形式分析了nodejs相關(guān)加密操作實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-03-03
Node.js 使用遞歸實(shí)現(xiàn)遍歷文件夾中所有文件
這篇文章主要介紹了Node.js使用遞歸實(shí)現(xiàn)遍歷文件夾中所有文件,需要的朋友可以參考下2017-09-09
Node.js API詳解之 vm模塊用法實(shí)例分析
這篇文章主要介紹了Node.js API詳解之 vm模塊用法,結(jié)合實(shí)例形式分析了Node.js API中vm模塊基本功能、函數(shù)、使用方法及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2020-05-05
讓nodeJS支持ES6的詞法----babel的安裝和使用方法
這篇文章主要介紹了讓nodeJS支持ES6的詞法----babel的安裝和使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07

