koa源碼部分解析(3)koa-body

版本4.1.1

慣例,先來看看簡單的使用

const Koa = require('koa');
const body = require('koa-body');

const app = new Koa();

// 可以給parsedMethods傳想要koa-body解析的方法
app.use(body({parsedMethods: ['POST', 'PUT', 'PATCH', 'GET', 'HEAD', 'DELETE']}));

app.use(async function(ctx) {
  ctx.body = '123123';
});

app.listen(3001);

koa-body自己的代碼其實沒幾行,先處理一堆參數(大多是傳給co-body和formidable用的),然後用type-is這個包(ctx.is函數)判斷出請求的數據類型,然後根據不同類型用co-body和formidable來解析,拿到解析結果以後放到body或者files裏面,齊活。

type-is

所以我們先來看看type-is是什麼神奇的包,雖然寫的挺複雜而且引了mime-types和media-typer,但是最終起作用的還是mime-db這個包,看名字就知道其實存了很多很多文件類型,總之type-is就是去匹配了下請求類型是什麼(通過content-type來判斷,而不是請求內容),來貼一段koa-body的代碼,我們也可以用ctx.is來判斷請求的類型,就抄下面的寫法就行

const jsonTypes = [
  'application/json',
  'application/json-patch+json',
  'application/vnd.api+json',
  'application/csp-report'
];

// 省略一些代碼

if (opts.json && ctx.is(jsonTypes)) {
  bodyPromise = buddy.json(ctx, {
    encoding: opts.encoding,
    limit: opts.jsonLimit,
    strict: opts.jsonStrict,
    returnRawBody: opts.includeUnparsed
  });
} else if (opts.urlencoded && ctx.is('urlencoded')) {
	// 對應application/x-www-form-urlencoded
  bodyPromise = buddy.form(ctx, {
    encoding: opts.encoding,
    limit: opts.formLimit,
    queryString: opts.queryString,
    returnRawBody: opts.includeUnparsed
  });
} else if (opts.text && ctx.is('text')) {
  bodyPromise = buddy.text(ctx, {
    encoding: opts.encoding,
    limit: opts.textLimit,
    returnRawBody: opts.includeUnparsed
  });
} else if (opts.multipart && ctx.is('multipart')) {
	// 用formidable來解析
  bodyPromise = formy(ctx, opts.formidable);
}

// 省略一些代碼
bodyPromise = bodyPromise || Promise.resolve({});

在type-is裏有一個函數叫hasbody,get請求在這個函數的判斷下被認爲沒有body,所以get請求獲取到的結果都是空對象,這個空對象來自上面代碼段中的最後一句,Promise.resolve({})。

function hasbody (req) {
  return req.headers['transfer-encoding'] !== undefined ||
    !isNaN(req.headers['content-length'])
}

function typeofrequest (req, types_) {
  var types = types_

  // no body
  if (!hasbody(req)) {
    return null
  }
  
  // 省略一些代碼
}

關於is函數的使用方法及輸出結果,koa本體的代碼註釋裏也有相對詳細的介紹,在request裏面。

co-body

接下來我們來看co-body,代碼結構很簡單,提供了json、form、text這三種格式的解析,主要依賴的是inflation和raw-body,先貼一段text的,最簡單,json和form也差不多,處理了一下參數(主要是encoding和limit參數),用inflation和raw-body解析了一把,返回,和koa-body一毛一樣

module.exports = function(req, opts){
  req = req.req || req;
  opts = utils.clone(opts);

  // defaults
  var len = req.headers['content-length'];
  var encoding = req.headers['content-encoding'] || 'identity';
  if (len && encoding === 'identity') opts.length = ~~len;
  opts.encoding = opts.encoding === undefined ? 'utf8': opts.encoding;
  opts.limit = opts.limit || '1mb';

  // raw-body returns a Promise when no callback is specified
  return Promise.resolve()
    .then(function() {
    	// 關鍵的一句
      return raw(inflate(req), opts);
    })
    .then(str => {
      // ensure return the same format with json / form
      return opts.returnRawBody ? { parsed: str, raw: str } : str;
    });
};

於是我們再來看inflation和raw-body是怎麼回事,inflation比較簡單,根據content-encoding的類型,做不同的操作,如果是gzip和deflate,調用zlib.Unzip解壓縮,如果是identity,直接返回輸入值。

至於raw-body,它做的事情是stream解析,關於stream的細節可以看看這篇https://blog.csdn.net/u011393161/article/details/104472698,還瞅見一個挺嚇人的包,iconv-lite,有興趣可以看看。

formidable

如果我們的數據類型是被type-is判斷爲multipart,那麼就會調用formidable來進行解析,formidable本身也提供了很多種格式的解析,有json,multipart,urlencoded等,所以實際上co-body和formidable有點重疊,感覺上有formidable就夠用了。

結構上formidable也是提供了各種格式各自的解析實現,主要是write和end兩個函數,因爲multipart格式的解析寫的比較複雜,所以我們可以先看看json格式或者urlencoded格式的解析,不過要將koa-body的代碼略做修改,不然這兩種格式是由co-body來解析的,不會進入formidable,這裏就不貼修改的代碼了,很簡單。

我們先來看看json的解析,formidable是通過下面這樣的正則對content-type進行判斷來決定解析格式的,和koa-body的判斷方法不一致,有點尷尬

this.headers['content-type'].match(/json/i)
this.headers['content-type'].match(/urlencoded/i)
this.headers['content-type'].match(/multipart/i)
this.headers['content-type'].match(/octet-stream/i) // 這種格式不太常見。。。吧

formidable的parse函數的主要流程是先在writeHeaders函數中調用_parseContentType,初始化對應格式的解析器,然後開始註冊stream的事件,當data事件註冊,stream進入流動模式,解析器中的write函數就開始執行,解析過程中還有一些自定義的事件,可以進行訂閱處理,koa-body中訂閱了error、field、file等事件,其實也可以通過回調函數來獲取解析的fields和files。

json格式的解析比較簡單,往chunks裏不斷寫數據就是了,multipart就比較複雜,構造一個multipart的數據都挺麻煩了,我也是第一次注意到multipart的content-type其實是像這樣的

multipart/form-data; boundary=--------------------------010958153996667037724890

form-data這個包可以在node端模擬FormData的數據,multipart_parser是根據傳輸的數據格式一塊一塊的分開來解析,可以看到multipart_parser裏面有個大的switch,裏面標記了各個部分,具體細節不展開了😂

結語

感覺比較細節的實現都沒講到,實在慚愧,不過一來自己沒有理的很清楚,二來是很多處理是流和buffer,不像普通的數據那麼容易看明白,哪天有空了回頭再來補一補

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章