koa-bodyparser 源码解析

前言

前面我们已经把【裸配】 Koa 学习了一下,学完了以后好像并没有什么好用的东西,然而这玩意就像一个巨大的平台,容易集成各种小插件,来达到各种各样的功能。接下来我们学习一个 koa-bodyparser 这歌短小精悍的库!

HelloWorld

当我的 hello world 如此简单的时候,我想发一个 POST 请求,那么我怎么拿到 POST 请求的参数呢,我们 ctx.request.body 是空的,Koa 原声的框架并没有帮我们解析 POST 的请求数据。所以我们就需要加入 koa-bodyparser 中间件,这样我们就可以通过 ctx.request.body 拿到数据了。

1
2
3
4
5
6
7
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
// ctx.body = ctx.query.param;
console.log( ctx.request.body)
});
app.listen(3000);

首先安装这个库:

1
npm install koa-bodyparser --save

然后再代码中加入这个中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Koa = require('koa');
const bodyParser = require('koa-bodyparser')

const app = new Koa();

app.use(bodyParser());

app.use(async ctx => {
ctx.body = ctx.request.body.param;
console.log(ctx.request.body)
});

app.listen(3000);

这时候 ctx.request.body 便是一个解析好的对象了,直接取对象的属性就可以了

题外话

原声的 NodeJS 通过 createServer 也是没有办法直接取到 post 的参数的,还是需要做一些读取数据的操作才可以

源码解析

bodyParser

作为一个中间件,需要返回一个 async 函数,在这个函数的最后调用 next(),关键部分是 parseBody 函数,将 request 进行解析,并将结果返回赋值给 ctx.request.body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
return async function bodyParser(ctx, next) {
if (ctx.request.body !== undefined) return await next();
if (ctx.disableBodyParser) return await next();
try {
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();
};
parseBody

ctx.request.is 可以检查 request 【Content-Type】 请求的类型是否是当中的一个,一般情况下以 form 居多,即 application/x-www-form-urlencoded

1
2
3
4
5
6
7
8
9
10
11
12
async function parseBody(ctx) {
if (enableJson && ((detectJSON && detectJSON(ctx)) || ctx.request.is(jsonTypes))) {
return await parse.json(ctx, jsonOpts);
}
if (enableForm && ctx.request.is(formTypes)) {
return await parse.form(ctx, formOpts);
}
if (enableText && ctx.request.is(textTypes)) {
return await parse.text(ctx, textOpts) || '';
}
return {};
}
parse.form

主要是 raw(inflate(req), opts) 方法,将请求的参数转化为一个 string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module.exports = async function(req, opts) {
req = req.req || req;
opts = utils.clone(opts);
const queryString = opts.queryString || {};

// keep compatibility with qs@4
if (queryString.allowDots === undefined) queryString.allowDots = true;

// defaults
const len = req.headers['content-length'];
const encoding = req.headers['content-encoding'] || 'identity';
if (len && encoding === 'identity') opts.length = ~~len;
opts.encoding = opts.encoding || 'utf8';
opts.limit = opts.limit || '56kb';
opts.qs = opts.qs || qs;

const str = await raw(inflate(req), opts);
try {
const parsed = opts.qs.parse(str, queryString);
return opts.returnRawBody ? { parsed, raw: str } : parsed;
} catch (err) {
err.status = 400;
err.body = str;
throw err;
}
};
raw

读取 body 数据,

1
2
3
4
5
6
7
8
9
function getRawBody (stream, options, callback) {

return new Promise(function executor (resolve, reject) {
readStream(stream, encoding, length, limit, function onRead (err, buf) {
if (err) return reject(err)
resolve(buf)
})
})
}

选取了 onData 函数,可以接受流数据,以字符串形式,结束后直接返回 字符串即可,数据为 buffer,使用 buffer.toString() 就可以转化为字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function readStream (stream, encoding, length, limit, callback) {
// attach listeners
stream.on('aborted', onAborted)
stream.on('close', cleanup)
stream.on('data', onData)
stream.on('end', onEnd)
stream.on('error', onEnd)

function onData (chunk) {
if (complete) return

received += chunk.length

if (limit !== null && received > limit) {
done(createError(413, 'request entity too large', {
limit: limit,
received: received,
type: 'entity.too.large'
}))
} else if (decoder) {
buffer += decoder.write(chunk)
} else {
buffer.push(chunk)
}
}

function onEnd (err) {
if (complete) return
if (err) return done(err)

if (length !== null && received !== length) {
done(createError(400, 'request size did not match content length', {
expected: length,
length: length,
received: received,
type: 'request.size.invalid'
}))
} else {
var string = decoder
? buffer + (decoder.end() || '')
: Buffer.concat(buffer)
done(null, string)
}
}
}

在 onEnd 中返回字符串

写在最后

koa-bodyparser 不仅做了 post 的转化,还有一些可选的参数,比如说传输类型,大小限制,这些都是可以通过参数进行配置的。源码博大精深啊!