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-parser1.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]是否显示最大小数位数,默认为falsethousandsSeparator[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