版本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是什么神奇的包,虽然写的挺复杂而且引了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,代码结构很简单,提供了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,有兴趣可以看看。
如果我们的数据类型是被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,不像普通的数据那么容易看明白,哪天有空了回头再来补一补