玩轉Koa -- koa-bodyparser原理解析

1、前置知識

  在理解koa-bodyparser原理以前,首先須要瞭解部分HTTP相關的知識。前端

一、報文主體

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

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

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

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

二、服務器端獲取報文主體流程

  HTTP底層採用TCP提供可靠的字節流服務,簡單而言就是報文主體部分會被轉化爲二進制數據在網絡中傳輸,因此服務器端首先須要拿到二進制流數據。segmentfault

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

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

  服務器端會根據報文頭部信息中的Content-Encoding確認採用何種解壓編碼。網絡

  接下來就須要將二進制數據轉換爲相應的字符,而字符也有不一樣的字符編碼方式,例如對於中文字符處理差別巨大的UTF-8和GBK,UTF-8編碼漢字一般須要三個字節,而GBK只須要兩個字節。因此還須要在請求報文的頭部信息中設置Content-Type使用的字符編碼信息(默認狀況下采用的是UTF-8),這樣服務器端就能夠利用相應的字符規則進行解碼,獲得正確的字符串。app

  拿到字符串以後,服務器端又要問了:客戶端,你這一段字符串是啥意思啊?框架

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

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

  客戶端會將採用的字符串編碼方式設置在請求報文頭部信息的Content-Type屬性中,這樣服務器端根據相應的字符串編碼規則進行解碼,就可以明白客戶端所傳遞的信息了。

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

2、獲取二進制數據流

  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】主要是對上述示例代碼的封裝和健壯性處理。

3、內容解碼

  客戶端會將內容編碼的方式放入請求報文頭部信息Content-Encoding屬性中,服務器端接收報文主體的二進制數據了時,會根據該頭部信息進行解壓操做,固然服務器端能夠在響應報文頭部信息Accept-Encoding屬性中添加支持的解壓方式。

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

4、字符解碼

  通常而言,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獲得

5、字符串解碼

  前面已經提到了字符串的二種編碼方式,它們對應的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,當採用表單上傳時,報文主體中會包含多個實體主體:

------WebKitFormBoundaryqsAGMB6Us6F7s3SF
Content-Disposition: form-data; name="image"; filename="image.png"
Content-Type: image/png


------WebKitFormBoundaryqsAGMB6Us6F7s3SF
Content-Disposition: form-data; name="text"

------WebKitFormBoundaryqsAGMB6Us6F7s3SF--

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

5、總結

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

  最後留圖一張:

往期精彩回顧

玩轉Koa -- koa-router原理解析

玩轉Koa -- 核心原理分析

相關文章
相關標籤/搜索