body-parser简介及源码解析
简介
body-parser
是一个HTTP请求体解析中间件,使用这个模块可以解析JSON、Raw、文本、URL-encoded
格式的请求体,Express框架中就是使用这个模块做为请求体解析中间件。
POST报文
一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成,一个常见的POST的请求报文格式如下:
POST /test/ HTTP/1.1
Accept-Encoding: gzip
Content-Length: 225873
Content-Type: text/plain; charset=utf8
Host: 127.0.0.1
Connection: Keep-Alive
huxinmin
Content-Type
:请求报文主体的类型、编码。常见的类型有text/plain
、application/json
、application/x-www-form-urlencoded
,multipart/form-data
。常见的编码有utf8
、gbk
等。Content-Encoding
:声明报文主体的压缩格式,常见的取值有gzip
、deflate
、identity
。- 报文主体:这里是个普通的文本字符串
huxinmin
原生环境解析
Node.js 原生HTTP模块中,是将用户请求数据封装到了用于请求对象req
中,该对象是一个IncomingMessage
,该对象同时也是一个可读流对象。在原生HTTP服务器,或不依赖第三方解析模块时,可以像下面这样接收并解析请求体:
const http = require('http');
//用http模块创建一个http服务端
http.createServer(function(req, res) {
if (req.method.toLowerCase() === 'post') {
var body = '';
req.on('data', function(chunk){
body += chunk;
});
req.on('end', function(){
if(req.headers['content-type'].indexOf('application/json')>=0){
// JSON 格式请求体解析
JSON.parse(body);
} else if(req.headers['content-type'].indexOf('application/octet-stream')>=0){
// Raw 格式请求体解析
// ……
} else if(req.headers['content-type'].indexOf('text/plain')>=0){
// text 文本格式请求体解析
// ……
} else if(req.headers['content-type'].indexOf('application/x-www-form-urlencoded')>=0){
// URL-encoded 格式请求体解析
// ……
} else {
// 其它格式解析
}
})
} else {
res.end('其它提交方式');
}
}).listen(3000);
body-parser
主要做了什么
- 处理不同类型的请求体:比如
text
、json
、urlencoded
等,对应的报文主体的格式不同。 - 处理不同的编码:比如
utf8
、gbk
等。 - 处理不同的压缩类型:比如
gzip
、deflare
等。 - 其他边界、异常的处理。
body-parser
使用方法以及API
注意body-parser
不支持multipart/form-data
的解析。
var bodyParser = require('body-parser')
-
bodyParser.json(options)
- 解析JSON格式
Options有如下这些选项:
inflate
- 设置为true时,deflate压缩数据会被解压缩;设置为true时,deflate压缩数据会被拒绝。默认为true。limit
- 设置请求的最大数据量。默认为'100kb'reviver
- 传递给JSON.parse()方法的第二个参数,详见JSON.parse()strict
- 设置为true时,仅会解析Array和Object两种格式;设置为false会解析所有JSON.parse
支持的格式。默认为truetype
- 该选项用于设置为指定MIME类型的数据使用当前解析中间件。这个选项可以是一个函数或是字符串,当是字符串是会使用type-is
来查找MIMI类型;当为函数是,中间件会通过fn(req)来获取实际值。默认为application/json
。verify
- 这个选项仅在verify(req, res, buf, encoding)
时受支持-
bodyParser.raw(options)
- 解析二进制格式
Options有如下这些选项:
inflate
- 设置为true时,deflate压缩数据会被解压缩;设置为true时,deflate压缩数据会被拒绝。默认为true。limit
- 设置请求的最大数据量。默认为'100kb'type
- 该选项用于设置为指定MIME类型的数据使用当前解析中间件。这个选项可以是一个函数或是字符串,当是字符串是会使用type-is
来查找MIMI类型;当为函数是,中间件会通过fn(req)来获取实际值。默认为application/octet-stream
。verify
- 这个选项仅在verify(req, res, buf, encoding)
时受支持-
bodyParser.text(options)
- 解析文本格式
Options有如下选项:
defaultCharset
- 如果Content-Type
后没有指定编码时,使用此编码。默认为'utf-8'inflate
- 设置为true时,deflate压缩数据会被解压缩;设置为true时,deflate压缩数据会被拒绝。默认为true。limit
- 设置请求的最大数据量。默认为'100kb'type
- 该选项用于设置为指定MIME类型的数据使用当前解析中间件。这个选项可以是一个函数或是字符串,当是字符串是会使用type-is
来查找MIMI类型;当为函数是,中间件会通过fn(req)来获取实际值。默认为application/octet-stream
。- verify - 这个选项仅在verify(req, res, buf, encoding)时受支持
-
bodyParser.urlencoded(options) - 解析文本格式
Options有如下这些选项:
extended
- 当设置为false时,会使用querystring库解析URL编码的数据;当设置为true时,会使用qs库解析URL编码的数据。后没有指定编码时,使用此编码。默认为trueinflate
- 设置为true时,deflate压缩数据会被解压缩;设置为true时,deflate压缩数据会被拒绝。默认为true。limit
- 设置请求的最大数据量。默认为'100kb'parameterLimit
- 用于设置URL编码值的最大数据。默认为1000type
- 该选项用于设置为指定MIME类型的数据使用当前解析中间件。这个选项可以是一个函数或是字符串,当是字符串是会使用type-is
来查找MIMI类型;当为函数是,中间件会通过fn(req)来获取实际值。默认为application/octet-stream
。verify
- 这个选项仅在verify(req, res, buf, encoding)时受支持
body-parser
依赖项简介
这是body-parser
1.18.3的依赖项配置:
"dependencies": {
"bytes": "3.0.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
}
bytes
一个将不同比特单位进行互转的工具,使用方法如下:
var bytes = require('bytes');
bytes(number|string,[options])
可选项有如下这些:
decimalPlaces
[number|null]输出最大小数位数,默认为2位fixedDecimals
[boolean|null]是否显示最大小数位数,默认为false
thousandsSeparator
[string|null]千位分割符,默认为空unit
[string|null]输出的单位((B/KB/MB/GB/TB),默认为空(自动检测)unitSeparator
[string|null]输出单位和数字之间分割符,默认为空
例子:
bytes(1024);
// output: '1KB'
bytes(1000);
// output: '1000B'
bytes(1000, {thousandsSeparator: ' '});
// output: '1 000B'
bytes(1024 * 1.7, {decimalPlaces: 0});
// output: '2KB'
bytes(1024, {unitSeparator: ' '});
// output: '1 KB'
bytes('1KB');
// output: 1024
bytes('1024');
// output: 1024
bytes(1024);
// output: 1024
content-type
创建并且解析http content-type
头,根据RFC7231,它有如下两个API:
var contentType = require('content-type')
contentType.parse(string|req|res)
解析一个content-type
并返回一个对象,这个对象包含两个属性,一个是type
,一个是parameters
。contentType.format(obj)
从一个对象中生成一个content-type
字符串,obj应包含type
和parameters
两个属性。
debug
一个轻量的调试js代码的库,具体可以参照我的这篇博客:Debug.js学习
depd
一个用于声明废弃属性或方法的库,具体可参考我的这篇博客depd学习教程
http-errors
http-errors
是一个用于创建HTTP错误的工具。使用方法很简单:
var createError = require('http-errors')
var err = createError([status], [message], [properties])
//或者
var err = new createError[code || name]([msg]))
//例如
var err = new createError.NotFound()
var err = createError(404, 'This video does not exist!')
生成的错误对象具有如下属性:
expose
- 作为是否应该发送到客户端的信号,默认为false
当错误状态码大于500headers
- 发送到客户端的header头属性,默认没有定义,若定义必须使用小写名message
- 错误消息status
- 错误状态码,statusCode
的镜像为了兼容性statusCode
- 错误状态码,默认500
iconv-lite
纯js实现的字符编码解码工具,它具有如下特点:
- 不需要原生汇编环境,可直接运行在操作系统或者沙箱环境中
- 在很多火热的项目中被广泛使用,例如
expressjs
,grunt
,yeoman
等 - 比node-iconv更快
- 直观的API
- 支持流Nodejs v0.10+以上
- 通过
browserify
可支持浏览器环境 - 包含typescript的类型定义文件
- 支持react-native(需要额外安装
buffer
和stream
模块)
基本使用方法:
var iconv = require('iconv-lite');
// 将buffer转换为字符串
str = iconv.decode(Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]), 'win1251');
// 将字符串转换为buffer
buf = iconv.encode("Sample input string", 'win1251');
// 检测是否支持该编码
iconv.encodingExists("us-ascii")
流式使用:
// 将二进制流转换为字符串
http.createServer(function(req, res) {
var converterStream = iconv.decodeStream('win1251');
req.pipe(converterStream);
converterStream.on('data', function(str) {
console.log(str); // Do something with decoded strings, chunk-by-chunk.
});
});
// 对流进行编码解码
fs.createReadStream('file-in-win1251.txt')
.pipe(iconv.decodeStream('win1251'))
.pipe(iconv.encodeStream('ucs2'))
.pipe(fs.createWriteStream('file-in-ucs2.txt'));
//所有的encode/decode 流操作都有一个 .collect(cb)方法来积累数据
http.createServer(function(req, res) {
req.pipe(iconv.decodeStream('win1251')).collect(function(err, body) {
assert(typeof body == 'string');
console.log(body); // full request body string
});
});
支持的编码格式:
- 所有的Nodejs原生编码:
utf8
,ucs2
/utf16-le
,ascii
,binary
,base64
,hex
- 额外的
unicode
编码:utf16
,utf16-be
,utf-7
,utf-7-imap
- 广泛的单字节编码:
Windows 125x
,ISO-8859
,IBM/DOS
,Macintosh
,KOI8
,以及所有iconv
库支持的编码,例如latin1
,us-ascii
- 广泛的多字节编码格式:
CP932
,CP936
,CP949
,CP950
,GB2312
,GBK
,GB18030
,Big5
,Shift_JIS
,EUC-JP
on-finished
该模块将会在HTTP request事件关闭,完成或者出错的时候执行一次回调函数。使用方法很简单:
var onFinished = require('on-finished')
onFinished(res, function (err, res) {}) //在res事件完成后执行一个回调
onFinished(req, function (err, req) {}) //在req事件完成之后执行一个回调
onFinished.isFinished(res) //判断res是否已经完成
onFinished.isFinished(req) //判断req是否已经完成
注意,不支持HTTP CONNECT 和HTTP Upgrade请求。
qs
具体可参考我的这篇博客qs.js学习教程
raw-body
该模块可以用来获取并验证二进制请求体流。使用方法如下:
var getRawBody = require('raw-body');
getRawBody(stream, [options], [callback]) //如果没有回调函数或者全局promise时则返回一个promise
它的options
有三个参数:
length
流的长度,如果流的内容总计未达到规定的长度,则返回一个400状态码的错误limit
请求体比特大小的限制,使用的是bytes
库进行格式化的,例如100
,50kb
等,如果超出大小限制,会返回一个413状态码的错误。encoding
编码解码格式,默认不会进行解码,直接返回buffer
实例,如果想要以utf-8
进行解码,可以将之设为true
,你可以使用任何iconv-lite
支持的编码格式。
实例:
var getRawBody = require('raw-body')
var http = require('http')
var server = http.createServer(function (req, res) {
getRawBody(req)
.then(function (buf) {
res.statusCode = 200
res.end(buf.length + ' bytes submitted')
})
.catch(function (err) {
res.statusCode = 500
res.end(err.message)
})
})
server.listen(3000)
type-is
该模块可以用来推断请求的内容类型。使用方法如下:
typeis(req, ['json']) // 'json'
typeis(req, ['html', 'json']) // 'json'
typeis(req, ['application/*']) // 'application/json'
typeis(req, ['application/json']) // 'application/json'
typeis(req, ['html']) // false
typeis.hasBody(req) //是否具有body
var mediaType = 'application/json'
typeis.is(mediaType, ['json']) // 'json'
typeis.is(mediaType, ['html', 'json']) // 'json'
typeis.is(mediaType, ['application/*']) // 'application/json'
typeis.is(mediaType, ['application/json']) // 'application/json'
typeis.is(mediaType, ['html']) // false
源码解析
body-parser
源码的目录结构是这样的:
index.js
代码如下:
var deprecate = require('depd')('body-parser')
var parsers = Object.create(null)
exports = module.exports = deprecate.function(bodyParser,
'bodyParser: use individual json/urlencoded middlewares')
/**
* JSON parser.
* @public
*/
Object.defineProperty(exports, 'json', {
configurable: true,
enumerable: true,
get: createParserGetter('json')
})
/**
* Raw parser.
* @public
*/
Object.defineProperty(exports, 'raw', {
configurable: true,
enumerable: true,
get: createParserGetter('raw')
})
/**
* Text parser.
* @public
*/
Object.defineProperty(exports, 'text', {
configurable: true,
enumerable: true,
get: createParserGetter('text')
})
/**
* URL-encoded parser.
* @public
*/
Object.defineProperty(exports, 'urlencoded', {
configurable: true,
enumerable: true,
get: createParserGetter('urlencoded')
})
/**
* Create a middleware to parse json and urlencoded bodies.
*
* @param {object} [options]
* @return {function}
* @deprecated
* @public
*/
function bodyParser (options) {
var opts = {}
// exclude type option
if (options) {
for (var prop in options) {
if (prop !== 'type') {
opts[prop] = options[prop]
}
}
}
var _urlencoded = exports.urlencoded(opts)
var _json = exports.json(opts)
return function bodyParser (req, res, next) {
_json(req, res, function (err) {
if (err) return next(err)
_urlencoded(req, res, next)
})
}
}
/**
* Create a getter for loading a parser.
* @private
*/
function createParserGetter (name) {
return function get () {
return loadParser(name)
}
}
/**
* Load a parser module.
* @private
*/
function loadParser (parserName) {
var parser = parsers[parserName]
if (parser !== undefined) {
return parser
}
// this uses a switch for static require analysis
switch (parserName) {
case 'json':
parser = require('./lib/types/json')
break
case 'raw':
parser = require('./lib/types/raw')
break
case 'text':
parser = require('./lib/types/text')
break
case 'urlencoded':
parser = require('./lib/types/urlencoded')
break
}
// store to prevent invoking require()
return (parsers[parserName] = parser)
}
这个文件暴露了一个模块,是一个废弃的方法bodyParser
,而且暴露了四个属性json
,text
,raw
,urlencoded
,这四个属性通过Object.defineProperty
设置了setter
方法,依赖了lib/types
目录下的四个文件暴露出来的模块。
在lib
目录下还有一个公共文件read.js
被lib/types
目录下的其他四个文件使用,代码如下所示:
'use strict'
var createError = require('http-errors')
var getBody = require('raw-body')
var iconv = require('iconv-lite')
var onFinished = require('on-finished')
var zlib = require('zlib')
module.exports = read
function read (req, res, next, parse, debug, options) {
var length
var opts = options
var stream
// flag as parsed
req._body = true
// read options
var encoding = opts.encoding !== null
? opts.encoding
: null
var verify = opts.verify
try {
// get the content stream
stream = contentstream(req, debug, opts.inflate)
length = stream.length
stream.length = undefined
} catch (err) {
return next(err)
}
// set raw-body options
opts.length = length
opts.encoding = verify
? null
: encoding
// assert charset is supported
if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) {
return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
charset: encoding.toLowerCase(),
type: 'charset.unsupported'
}))
}
// read body
debug('read body')
getBody(stream, opts, function (error, body) {
if (error) {
var _error
if (error.type === 'encoding.unsupported') {
// echo back charset
_error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
charset: encoding.toLowerCase(),
type: 'charset.unsupported'
})
} else {
// set status code on error
_error = createError(400, error)
}
// read off entire request
stream.resume()
onFinished(req, function onfinished () {
next(createError(400, _error))
})
return
}
// verify
if (verify) {
try {
debug('verify body')
verify(req, res, body, encoding)
} catch (err) {
next(createError(403, err, {
body: body,
type: err.type || 'entity.verify.failed'
}))
return
}
}
// parse
var str = body
try {
debug('parse body')
str = typeof body !== 'string' && encoding !== null
? iconv.decode(body, encoding)
: body
req.body = parse(str)
} catch (err) {
next(createError(400, err, {
body: str,
type: err.type || 'entity.parse.failed'
}))
return
}
next()
})
}
function contentstream (req, debug, inflate) {
var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase()
var length = req.headers['content-length']
var stream
debug('content-encoding "%s"', encoding)
if (inflate === false && encoding !== 'identity') {
throw createError(415, 'content encoding unsupported', {
encoding: encoding,
type: 'encoding.unsupported'
})
}
switch (encoding) {
case 'deflate':
stream = zlib.createInflate()
debug('inflate body')
req.pipe(stream)
break
case 'gzip':
stream = zlib.createGunzip()
debug('gunzip body')
req.pipe(stream)
break
case 'identity':
stream = req
stream.length = length
break
default:
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
encoding: encoding,
type: 'encoding.unsupported'
})
}
return stream
}
该文件将主要是将请求流进行处理,并使用raw-body
模块解析出req.body
。至于其他json.js
,text.js
,raw.js
,urlencoded.js
都是针对不同的类型具体的一些异常处理或者边界判断等,源码就不一一分析了。
原生Nodejs实例
首先我们写一个客户端发送请求client.js
,代码如下:
var http = require('http');
var options = {
hostname: '127.0.0.1',
port: '8080',
path: '/test',
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'Content-Encoding': 'identity'
}
};
var client = http.request(options, (res) => {
res.pipe(process.stdout);
});
client.end('huxinmin');
然后新建一个server.js
服务端代码如下:
var http = require('http');
var bodyParser = require('./index');
// 创建服务器
http.createServer(function(req, res) {
var textParser = bodyParser.text();
textParser(req, res, function(err) {
console.log(req.body)
res.writeHead(200, { 'Content-Type': 'text/html' });
var data = { message: 'huxinmin is send complete' }
// 响应文件内容
res.write(JSON.stringify(data));
// 发送响应数据
res.end();
})
}).listen(8080);
// 控制台会输出以下信息
console.log('Server running at http://127.0.0.1:8080/');
启动代码,会看到前台返回了:
{"message":"huxinmin is send complete"}
服务端打印了输出:
huxinmin