玩轉Koa — koa-bodyparser原理解析

descire發表於2019-01-16

一、前置知識

  在理解koa-bodyparser原理之前,首先需要了解部分HTTP相關的知識。

1、報文主體

  HTTP報文主要分為請求報文和響應報文,koa-bodyparser主要針對請求報文的處理。

  請求報文主要由以下三個部分組成:

  • 報文頭部
  • 空行
  • 報文主體

  而koa-bodyparser中的body指的就是請求報文中的報文主體部分。

2、伺服器端獲取報文主體流程

  HTTP底層採用TCP提供可靠的位元組流服務,簡單而言就是報文主體部分會被轉化為二進位制資料在網路中傳輸,所以伺服器端首先需要拿到二進位制流資料。

  談到網路傳輸,當然會涉及到傳輸速度的優化,而其中一種優化方式就是對內容進行壓縮編碼,常用的壓縮編碼方式有:

  • gzip
  • compress
  • deflate
  • identity(不執行壓縮或不會變化的預設編碼格式)

  伺服器端會根據報文頭部資訊中的Content-Encoding確認採用何種解壓編碼。

  接下來就需要將二進位制資料轉換為相應的字元,而字元也有不同的字元編碼方式,例如對於中文字元處理差異巨大的UTF-8和GBK,UTF-8編碼漢字通常需要三個位元組,而GBK只需要兩個位元組。所以還需要在請求報文的頭部資訊中設定Content-Type使用的字元編碼資訊(預設情況下采用的是UTF-8),這樣伺服器端就可以利用相應的字元規則進行解碼,得到正確的字串。

  拿到字串之後,伺服器端又要問了:客戶端,你這一段字串是啥意思啊?

  根據不同的應用場景,客戶端會對字串採用不同的編碼方式,常見的編碼方式有:

  • URL編碼方式: a=1&
    b=2
  • JSON編碼方式: {a:1,b:2
    }

  客戶端會將採用的字串編碼方式設定在請求報文頭部資訊的Content-Type屬性中,這樣伺服器端根據相應的字串編碼規則進行解碼,就能夠明白客戶端所傳遞的資訊了。

  下面一步步分析koa-bodyparser是如何處理這一系列操作,從而得到報文主體內容。

二、獲取二進位制資料流

  NodeJS中獲取請求報文主體二進位制資料流主要通過監聽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) // 接收到的二進位制資料流 // 利用res.end進行響應處理 res.end(chunks.toString())
})
}).listen(1234)複製程式碼

  而koa-bodyparser主要是對co-body的封裝,而【co-body】中主要是採用raw-body模組獲取請求報文主體的二進位制資料流,【row-body】主要是對上述示例程式碼的封裝和健壯性處理。

三、內容解碼

  客戶端會將內容編碼的方式放入請求報文頭部資訊Content-Encoding屬性中,伺服器端接收報文主體的二進位制資料了時,會根據該頭部資訊進行解壓操作,當然伺服器端可以在響應報文頭部資訊Accept-Encoding屬性中新增支援的解壓方式。

  而【row-body】主要採用inflation模組進行解壓處理。

四、字元解碼

  一般而言,UTF-8是網際網路中主流的字元編碼方式,前面也提到了還有GBK編碼方式,相比較UTF-8,它編碼中文只需要2個位元組,那麼在字元解碼時誤用UTF-8解碼GBK編碼的字元,就會出現中文亂碼的問題。

  NodeJS主要通過Buffer處理二進位制資料流,但是它並不支援GBK字元編碼方式,需要通過iconv-lite模組進行處理。

  【示例一】中的程式碼就存在沒有正確處理字元編碼的問題,那麼報文主體中的字元采用GBK編碼方式,必然會出現中文亂碼:

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) // 發生中文亂碼情況
})複製程式碼

  NodeJS中的Buffer預設是採用UTF-8字元編碼處理,這裡藉助【iconv-lite】模組處理不同的字元編碼方式:

    const chunks = Buffer.concat(body)    res.end(iconv.decode(chunks, charset)) // charset通過Content-Type得到複製程式碼

五、字串解碼

  前面已經提到了字串的二種編碼方式,它們對應的Content-Type分別為:

  • URL編碼 application/x-www-form-urlencoded
  • JSON編碼 application/json

  對於前端來說,URL編碼並不陌生,經常會用於URL拼接操作,唯一需要注意的是不要忘記對鍵值對進行decodeURIComponent()處理。

  當客戶端傳送請求主體時,需要進行編碼操作:

  'a=1&
b=2&
c=3'
複製程式碼

  伺服器端再根據URL編碼規則解碼,得到相應的物件。

  // URL編碼方式 簡單的解碼方法實現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編碼方式適合處理簡單的鍵值對資料,並且很多框架的Ajax中的Content-Type預設值都是它,但是對於複雜的巢狀物件就不太好處理了,這時就需要JSON編碼方式大顯身手了。

  客戶端傳送請求主體時,只需要採用JSON.stringify進行編碼。伺服器端只需要採用JSON.parse進行解碼即可:

const strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/;
function parse(str) {
if (!strict) return str ? JSON.parse(str) : str;
// 嚴格模式下,總是返回一個物件 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中通過判斷當前Content-Type型別,呼叫不同的處理方式,將獲取到的結果掛載在ctx.request.body:

  return async function bodyParser(ctx, next) { 
if (ctx.request.body !== undefined) return await next();
if (ctx.disableBodyParser) return await next();
try {
// 最重要的一步 將解析的內容掛載到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 {
};

}
};
複製程式碼

  其實還有一種比較常見的Content-type,當採用表單上傳時,報文主體中會包含多個實體主體:

------WebKitFormBoundaryqsAGMB6Us6F7s3SFContent-Disposition: form-data;
name="image";
filename="image.png"Content-Type: image/png------WebKitFormBoundaryqsAGMB6Us6F7s3SFContent-Disposition: form-data;
name="text"------WebKitFormBoundaryqsAGMB6Us6F7s3SF--複製程式碼

  這種方式處理相對比較複雜,koa-bodyparser中並沒有提供該Content-Type的解析。(下一篇中應該會介紹^_^)

五、總結

  以上便是koa-bodyparser的核心實現原理,其中涉及到很多關於HTTP的基礎知識,對於HTTP不太熟悉的同學,可以推薦看一波入門級寶典【圖解HTTP】。

  最後留圖一張:

玩轉Koa — koa-bodyparser原理解析
往期精彩回顧

玩轉Koa — koa-router原理解析

玩轉Koa — 核心原理分析

來源:https://juejin.im/post/5c3de636f265da6179750d2b#comment

相關文章