简介

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/plainapplication/jsonapplication/x-www-form-urlencodedmultipart/form-data。常见的编码有utf8gbk等。
  • Content-Encoding:声明报文主体的压缩格式,常见的取值有gzipdeflateidentity
  • 报文主体:这里是个普通的文本字符串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主要做了什么

  • 处理不同类型的请求体:比如textjsonurlencoded等,对应的报文主体的格式不同。
  • 处理不同的编码:比如utf8gbk等。
  • 处理不同的压缩类型:比如gzipdeflare等。
  • 其他边界、异常的处理。

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支持的格式。默认为true
  • type - 该选项用于设置为指定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编码的数据。后没有指定编码时,使用此编码。默认为true
  • inflate - 设置为true时,deflate压缩数据会被解压缩;设置为true时,deflate压缩数据会被拒绝。默认为true。
  • limit - 设置请求的最大数据量。默认为'100kb'
  • parameterLimit - 用于设置URL编码值的最大数据。默认为1000
  • type - 该选项用于设置为指定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]是否显示最大小数位数,默认为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应包含typeparameters两个属性。

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当错误状态码大于500
  • headers - 发送到客户端的header头属性,默认没有定义,若定义必须使用小写名
  • message - 错误消息
  • status - 错误状态码,statusCode的镜像为了兼容性
  • statusCode - 错误状态码,默认500

iconv-lite

纯js实现的字符编码解码工具,它具有如下特点:

  • 不需要原生汇编环境,可直接运行在操作系统或者沙箱环境中
  • 在很多火热的项目中被广泛使用,例如expressjsgruntyeoman
  • node-iconv更快
  • 直观的API
  • 支持流Nodejs v0.10+以上
  • 通过browserify可支持浏览器环境
  • 包含typescript的类型定义文件
  • 支持react-native(需要额外安装bufferstream模块)

基本使用方法:

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库支持的编码,例如latin1us-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库进行格式化的,例如10050kb等,如果超出大小限制,会返回一个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,而且暴露了四个属性jsontextrawurlencoded,这四个属性通过Object.defineProperty设置了setter方法,依赖了lib/types目录下的四个文件暴露出来的模块。

lib目录下还有一个公共文件read.jslib/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.jstext.jsraw.jsurlencoded.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

参考资料