body-parser 源码分析
body-parser 源码分析
预备知识:熟悉 express 的中间件逻辑
阅读事件:30min
1. body-parser 解决什么问题
在 node http 模块中,您只能通过 data 事件,以 buffer 的形式来获取请求体内容,node 没有提供如何解析请求body的API,body-parser 提供了这个功能。
body-parser 本质是一个处理请求 body 的中间件函数,他负责按照您给的规则解析请求body,并且将结果赋值到 req.body 属性上。
2. 简单的使用 body-parser
var express = require('express')
var bodyParser = require('body-parser')
var app = express()
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))
// parse application/json
app.use(bodyParser.json())
app.use(function (req, res) {
res.setHeader('Content-Type', 'text/plain')
res.write('you posted:\n')
// 您可以通过req.body 来访问请求体内容
res.end(JSON.stringify(req.body, null, 2))
})
通过这个例子您可以了解到如何简单的使用 body-parser。
3. 源码分析
首先 bodyParser 的源码结构如下:
- index.js:入口文件
- lib:核心方法
- types:该文件下的4个文件,分别用于解析对应的4个类型
- json.js:将body解析为JSON对象
- raw.js
- text.js:将body解析为字符串
- urlencoded.js:将表单数据(urlencoded编码)解析为JSON对象
- read.js:读取 body 内容
- types:该文件下的4个文件,分别用于解析对应的4个类型
1. bodyParser的导出形式
bodyParser 的定义在 index.js,这里的逻辑非常清晰:
- 创建一个用于解析 json 和 urlencoded 格式的中间件:bodyParser 并导出
- 给 bodyParser 添加 json/text/raw/urlencoded 方法
'use strict'
var deprecate = require('depd')('body-parser')
// 缓存 parser
var parsers = Object.create(null)
// 导出一个Function
exports = module.exports = deprecate.function(bodyParser,
'bodyParser: use individual json/urlencoded middlewares')
// JSON parser.
Object.defineProperty(exports, 'json', {
configurable: true,
enumerable: true,
get: createParserGetter('json')
})
// Raw parser.
Object.defineProperty(exports, 'raw', {
configurable: true,
enumerable: true,
get: createParserGetter('raw')
})
// Text parser.
Object.defineProperty(exports, 'text', {
configurable: true,
enumerable: true,
get: createParserGetter('text')
})
// URL-encoded parser.
Object.defineProperty(exports, 'urlencoded', {
configurable: true,
enumerable: true,
get: createParserGetter('urlencoded')
})
// 创建一个用于解析 json 和 urlencoded 格式的中间件
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.
function createParserGetter (name) {
return function get () {
return loadParser(name)
}
}
// Load a parser module.
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)
}
4. text 解析流程
将 body 解析非常简单,这只需要将 buffer 转换为 string即可。 所以从最简单 text parser 开始,其他解析大体也是类似的,主要区别在于将字符串解析到特定格式的方法。比如将表单数据(urlencoded form) 解析为JSON对象。
现在您希望将 text/plain 的请求体解析为一个字符串,源码是这样的:
// 默认将 type 为 text/plain 解析为字符串
var express = require('express')
var bodyParser = require('body-parser')
var app = express()
var port = 3000;
app.use(bodyParser.text())
app.post('/text', (req, res) => res.send(req.body))
app.listen(port, () => console.log(`\nExample app listening on port ${port}!`))
当我们 curl 进行如下访操作:
$ curl -d "hello" http://localhost:3000/text
hello
这背后的流程是怎样的呢?
1. bodyParser.text() 中间件
由于我们使用 bodyParser.text() 中间件,所以当进行上述访问时,会访问到 lib/types/text,源码如下:
'use strict'
var bytes = require('bytes')
var contentType = require('content-type')
var debug = require('debug')('body-parser:text')
var read = require('../read')
var typeis = require('type-is')
// 导出 text 中间件
module.exports = text
// text 中间件 定义
function text (options) {
// option 是使用该中间件传入的选项
var opts = options || {}
// 获取字符集
var defaultCharset = opts.defaultCharset || 'utf-8'
// 是否处理压缩的body, true时body会被解压,false时body不会被处理
var inflate = opts.inflate !== false
// body大小限制
var limit = typeof opts.limit !== 'number'
? bytes.parse(opts.limit || '100kb')
: opts.limit
// 需要处理的 content-type 类型
var type = opts.type || 'text/plain'
// 用户自定义的校验函数,若提供则会被调用verify(req, res, buf, encoding)
var verify = opts.verify || false
if (verify !== false && typeof verify !== 'function') {
throw new TypeError('option verify must be function')
}
// create the appropriate type checking function
var shouldParse = typeof type !== 'function'
? typeChecker(type)
: type
// 这里是核心, 不同的解析器有不同的处理方式
// text parse 很简单是因为它啥也不需要干
function parse (buf) {
return buf
}
return function textParser (req, res, next) {
// 当我们进行 POST 请求时 textParser 中间件会被调用
// 这里先判断 body 是否已经解析过了,下游会设置为 true
if (req._body) {
debug('body already parsed')
next()
return
}
req.body = req.body || {}
// 没有请求体时不处理
// skip requests without bodies
if (!typeis.hasBody(req)) {
debug('skip empty body')
next()
return
}
debug('content-type %j', req.headers['content-type'])
// determine if request should be parsed
if (!shouldParse(req)) {
debug('skip parsing')
next()
return
}
// get charset
var charset = getCharset(req) || defaultCharset
// read
read(req, res, next, parse, debug, {
encoding: charset,
inflate: inflate,
limit: limit,
verify: verify
})
}
}
// 获取请求字符集
function getCharset (req) {
try {
return (contentType.parse(req).parameters.charset || '').toLowerCase()
} catch (e) {
return undefined
}
}
// content-type 检测
function typeChecker (type) {
return function checkType (req) {
return Boolean(typeis(req, type))
}
}
// 判断是否包含请求体(这个函数是从type-is包复制出来的)
function hasbody (req) {
return req.headers['transfer-encoding'] !== undefined ||
!isNaN(req.headers['content-length'])
}
大概流程如下:
- 使用 app.use 使用中间件
- 客户端发起 POST 请求
- 进入 textParser 中间件
- 判断是否已经解析过(req._body = true)
- 判断请求是否包含请求体
- 判断请求体类型是否需要处理
- 读取请求体,解析并设置 req.body && req._body = true
- 进入 read 中间件(读取请求体,解析并设置 req.body && req._body = true)
2. read() 中间件(lib/read.js)
lib/types 下的4个文件,最终都会访问 lib/read.js,形式如下:
read(req, res, next, parse, debug, {
encoding: charset,
inflate: inflate,
limit: limit,
verify: verify
})
现在我们来看下 lib/read.js 源码:
'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
// parsed flag, 上游服务有做判断
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 中读取内容
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
}
}
var str = body
try {
debug('parse body')
// 如果body不是字符类型而且设置了encoding,那么需要重新解码
str = typeof body !== 'string' && encoding !== null
? iconv.decode(body, encoding)
: body
// 这里不同解析器,会传入不同 parse
req.body = parse(str)
} catch (err) {
next(createError(400, err, {
body: str,
type: err.type || 'entity.parse.failed'
}))
return
}
next()
})
}
// 获取请求体 stream
// 1. 获取压缩编码格式,如果有压缩需要先解压
// 2. 返回 stream
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
}
5. 一些疑问
1. 为什么要对 charset 进行处理
其实本质上来说,charset前端一般都是固定为utf-8的, 甚至在JQuery的AJAX请求中,前端请求charset甚至是不可更改,只能是charset,但是在使用fetch等API的时候,的确是可以更改charset的,这个工作尝试满足一些比较偏僻的更改charset需求。
2. 为什么要对 content-encoding 做处理
一般情况下我们认为,考虑到前端发的AJAX之类的请求的数据量,是不需要做Gzip压缩的。但是向服务器发起请求的不一定只有前端,还可能是Node的客户端。这些Node客户端可能会向Node服务端传送压缩过后的数据流。 例如下面的代码所示:
const zlib = require('zlib');
const request = require('request');
const data = zlib.gzipSync(Buffer.from("我是一个被Gzip压缩后的数据"));
request({
method: 'POST',
url: 'http://127.0.0.1:3000/post',
headers: {//设置请求头
"Content-Type": "text/plain",
"Content-Encoding": "gzip"
},
body: data
})
6. 参考以及延伸
- npm bodyParser https://www.npmjs.com/package/body-parser#bodyparsertextoptions
- npm iconv-lite 纯JS编码转换器
- npm raw-body 以buffer或者string的方式获取一个可读流的全部内容,并且可校验长度
- bodyparser实现原理解析(这篇文章回答了我上述2个疑问) https://zhuanlan.zhihu.com/p/78482006
body-parser 源码分析的更多相关文章
- EasyUI学习总结(四)——parser源码分析
parser模块是easyloader第一个加载的模块,它的主要作用,就是扫描页面上easyui开头的class标签,然后初始化成easyui控件. /** * parser模块主要是解析页面中eas ...
- EasyUI学习总结(四)——parser源码分析(转载)
本文转载自:http://www.cnblogs.com/xdp-gacl/p/4082561.html parser模块是easyloader第一个加载的模块,它的主要作用,就是扫描页面上easyu ...
- 【Canal源码分析】parser工作过程
本文主要分析的部分是instance启动时,parser的一个启动和工作过程.主要关注的是AbstractEventParser的start()方法中的parseThread. 一.序列图 二.源码分 ...
- 大数据之Oozie——源码分析(一)程序入口
工作中发现在oozie中使用sqoop与在shell中直接调度sqoop性能上有很大的差异.为了更深入的探索其中的缘由,开始了oozie的源码分析之路.今天第一天阅读源码,由于没有编译成功,不能运行测 ...
- MyBatis源码分析-MyBatis初始化流程
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...
- angular源码分析:angular中脏活累活承担者之$parse
我们在上一期中讲 $rootscope时,看到$rootscope是依赖$prase,其实不止是$rootscope,翻看angular的源码随便翻翻就可以发现很多地方是依赖于$parse的.而$pa ...
- Tomcat源码分析
前言: 本文是我阅读了TOMCAT源码后的一些心得. 主要是讲解TOMCAT的系统框架, 以及启动流程.若有错漏之处,敬请批评指教! 建议: 毕竟TOMCAT的框架还是比较复杂的, 单是从文字上理解, ...
- Solr初始化源码分析-Solr初始化与启动
用solr做项目已经有一年有余,但都是使用层面,只是利用solr现有机制,修改参数,然后监控调优,从没有对solr进行源码级别的研究.但是,最近手头的一个项目,让我感觉必须把solrn内部原理和扩展机 ...
- MyBatis源码分析(1)-MapConfig文件的解析
1.简述 MyBatis是一个优秀的轻ORM框架,由最初的iBatis演化而来,可以方便的完成sql语句的输入输出到java对象之间的相互映射,典型的MyBatis使用的方式如下: String re ...
- Tomcat源码分析——SERVER.XML文件的加载与解析
前言 作为Java程序员,对于Tomcat的server.xml想必都不陌生.本文基于Tomcat7.0的Java源码,对server.xml文件是如何加载和解析的进行分析. 加载 server.xm ...
随机推荐
- 第15.42节、PyQt输入部件:QFontComboBox、QLineEdit、QTextEdit、QPlainText功能详解
专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 一.引言 输入部件量比较多,且功能很丰富,但除了用于编写编辑器.浏览器 ...
- PyQt(Python+Qt)学习随笔:QListWidget的currentRow属性
QListWidget的currentRow属性保存当前项的位置,为整型,从0开始计数,在某些选择模式下,当前项可能也是选中项. currentRow属性可以通过方法currentRow().setC ...
- Javascrip之BOM
重点内容 理解windows对象-BOM核心 控制窗口.框架.弹出窗口 利用location对象中的页面信息 利用navigator对象了解浏览器 BOM:浏览器对象模型[Browner Object ...
- 十. Axios网络请求封装
1. 网络模块的选择 Vue中发送网络请求有非常多的方式,那么在开发中如何选择呢? 选择一:传统的Ajax是基于XMLHttpRequest(XHR) 为什么不用它呢?非常好解释配置和调用方式等非常混 ...
- Java 中的语法糖,真甜。
我把自己以往的文章汇总成为了 Github ,欢迎各位大佬 star https://github.com/crisxuan/bestJavaer 我们在日常开发中经常会使用到诸如泛型.自动拆箱和装箱 ...
- deepFM(原理和pytorch理解)
参考(推荐):https://blog.csdn.net/w55100/article/details/90295932 要点: 其中的计算优化值得注意 K代表隐向量维数 n可以代表离散值one-ho ...
- 第二篇 Scrum 冲刺博客
一.站立式会议 1. 会议照片 2. 工作汇报 成员名称 昨日(23日)完成的工作 今天(24日)计划完成的工作 工作中遇到的困难 陈锐基 - 完成个人资料编辑功能- 对接获取表白动态的接口数据并渲染 ...
- box-sizing什么时候用?常用的值都有什么?
一般在做自适应的网页设计的时候用,用这个属性网页结构才不会被破坏. 常用的值: 1. content-box:宽度和高度分别应用到元素的内容框,在宽度和高度之外绘制元素的内边距和边框. 2. bo ...
- Java并发编程的艺术(四)——JMM、重排序、happens-before
什么是JMM JMM就是Java内存模型.目的是为了屏蔽系统和硬件的差异,让同一代码在不同平台下能够达到相同的访问结果.规定了线程和内存之间的关系. 内存划分 JMM规定了内存主要划分为主内存和工作内 ...
- Hive基础语法5分钟速览
Hive是基于Hadoop的一个数据仓库工具,可以将结构化的数据文件映射为一张数据库表,并提供简单的sql查询功能,可以将sql语句转换为MapReduce任务进行运行. 其优点是学习成本低,可以通过 ...