版本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,不像普通的數據那麼容易看明白,哪天有空了回頭再來補一補