深入koa-bodyparser原理解析


Posted in Javascript onJanuary 16, 2019

一、前置知识

在理解koa-bodyparser原理之前,首先需要了解部分HTTP相关的知识。

1、报文主体

HTTP报文主要分为请求报文和响应报文,koa-bodyparser主要针对请求报文的处理。

请求报文主要由以下三个部分组成:

  • 报文头部
  • 空行
  • 报文主体

而koa-bodyparser中的body指的就是请求报文中的报文主体部分。

2、服务器端获取报文主体流程

HTTP底层采用TCP提供可靠的字节流服务,简单而言就是报文主体部分会被转化为二进制数据在网络中传输,所以服务器端首先需要拿到二进制流数据。

谈到网络传输,当然会涉及到传输速度的优化,而其中一种优化方式就是对内容进行压缩编码,常用的压缩编码方式有:

  • gzip
  • compress
  • deflate
  • identity(不执行压缩或不会变化的默认编码格式)

服务器端会根据报文头部信息中的Content-Encoding确认采用何种解压编码。

接下来就需要将二进制数据转换为相应的字符,而字符也有不同的字符编码方式,例如对于中文字符处理差异巨大的UTF-8和GBK,UTF-8编码汉字通常需要三个字节,而GBK只需要两个字节。所以还需要在请求报文的头部信息中设置Content-Type使用的字符编码信息(默认情况下采用的是UTF-8),这样服务器端就可以利用相应的字符规则进行解码,得到正确的字符串。

拿到字符串之后,服务器端又要问了:客户端,你这一段字符串是啥意思啊?

根据不同的应用场景,客户端会对字符串采用不同的编码方式,常见的编码方式有:

  • URL编码方式: a=1&b=2
  • JSON编码方式: {a:1,b:2}

客户端会将采用的字符串编码方式设置在请求报文头部信息的Content-Type属性中,这样服务器端根据相应的字符串编码规则进行解码,就能够明白客户端所传递的信息了。

下面一步步分析koa-bodyparser是如何处理这一系列操作,从而得到报文主体内容。

二、获取二进制数据流

NodeJS中获取请求报文主体二进制数据流主要通过监听request对象的data事件完成:

// 示例一
const http = require('http')

http.createServer((req, res) => {
 const body = []

 req.on('data', chunk => {
  body.push(chunk)
 })
 
 req.on('end', () => {
  const chunks = Buffer.concat(body) // 接收到的二进制数据流

  // 利用res.end进行响应处理
  res.end(chunks.toString())
 })
}).listen(1234)

而koa-bodyparser主要是对co-body 的封装,而【co-body】中主要是采用raw-body 模块获取请求报文主体的二进制数据流,【row-body】主要是对上述示例代码的封装和健壮性处理。

三、内容解码

客户端会将内容编码的方式放入请求报文头部信息Content-Encoding属性中,服务器端接收报文主体的二进制数据了时,会根据该头部信息进行解压操作,当然服务器端可以在响应报文头部信息Accept-Encoding属性中添加支持的解压方式。

而【row-body】主要采用 inflation 模块进行解压处理。

四、字符解码

一般而言,UTF-8是互联网中主流的字符编码方式,前面也提到了还有GBK编码方式,相比较UTF-8,它编码中文只需要2个字节,那么在字符解码时误用UTF-8解码GBK编码的字符,就会出现中文乱码的问题。

NodeJS主要通过Buffer处理二进制数据流,但是它并不支持GBK字符编码方式,需要通过 iconv-lite 模块进行处理。

【示例一】中的代码就存在没有正确处理字符编码的问题,那么报文主体中的字符采用GBK编码方式,必然会出现中文乱码:

const request = require('request')
const iconv = require('iconv-lite')

request.post({
 url: 'http://localhost:1234/',
 body: iconv.encode('中文', 'gbk'),
 headers: {
  'Content-Type': 'text/plain;charset=GBK'
 }
}, (error, response, body) => {
 console.log(body) // 发生中文乱码情况
})

NodeJS中的Buffer默认是采用UTF-8字符编码处理,这里借助【iconv-lite】模块处理不同的字符编码方式:

const chunks = Buffer.concat(body)
  res.end(iconv.decode(chunks, charset)) // charset通过Content-Type得到

五、字符串解码

前面已经提到了字符串的二种编码方式,它们对应的Content-Type分别为:

  • URL编码 application/x-www-form-urlencoded
  • JSON编码 application/json

对于前端来说,URL编码并不陌生,经常会用于URL拼接操作,唯一需要注意的是不要忘记对键值对进行decodeURIComponent()处理。

当客户端发送请求主体时,需要进行编码操作:

'a=1&b=2&c=3'

服务器端再根据URL编码规则解码,得到相应的对象。

// URL编码方式 简单的解码方法实现
function decode (qs, sep = '&', eq = '=') {
 const obj = {}
 qs = qs.split(sep)

 for (let i = 0, max = qs.length; i < max; i++) {
  const item = qs[i]
  const index = item.indexOf(eq)

  let key, value

  if (~index) {
   key = item.substr(0, index)
   value = item.substr(index + 1)
  } else {
   key = item
   value = ''
  }
  
  key = decodeURIComponent(key)
  value = decodeURIComponent(value)

  if (!obj.hasOwnProperty(key)) {
   obj[key] = value
  }
 }
 return obj
}

console.log(decode('a=1&b=2&c=3')) // { a: '1', b: '2', c: '3' }

URL编码方式适合处理简单的键值对数据,并且很多框架的Ajax中的Content-Type默认值都是它,但是对于复杂的嵌套对象就不太好处理了,这时就需要JSON编码方式大显身手了。

客户端发送请求主体时,只需要采用JSON.stringify进行编码。服务器端只需要采用JSON.parse进行解码即可:

const strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/;
function parse(str) {
 if (!strict) return str ? JSON.parse(str) : str;
 // 严格模式下,总是返回一个对象
 if (!str) return {};
 // 是否为合法的JSON字符串
 if (!strictJSONReg.test(str)) {
  throw new Error('invalid JSON, only supports object and array');
 }
 return JSON.parse(str);
}

除了上述两种字符串编码方式,koa-bodyparser还支持不采用任何字符串编码方式的普通字符串。

三种字符串编码的处理方式由【co-body】模块提供,koa-bodyparser中通过判断当前Content-Type类型,调用不同的处理方式,将获取到的结果挂载在ctx.request.body:

return async function bodyParser(ctx, next) {
  if (ctx.request.body !== undefined) return await next();
  if (ctx.disableBodyParser) return await next();
  try {
   // 最重要的一步 将解析的内容挂载到koa的上下文中
   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();
 };

 async function parseBody(ctx) {
  if (enableJson && ((detectJSON && detectJSON(ctx)) || ctx.request.is(jsonTypes))) {
   return await parse.json(ctx, jsonOpts); // application/json等json type
  }
  if (enableForm && ctx.request.is(formTypes)) {
   return await parse.form(ctx, formOpts); // application/x-www-form-urlencoded
  }
  if (enableText && ctx.request.is(textTypes)) {
   return await parse.text(ctx, textOpts) || ''; // text/plain
  }
  return {};
 }
};

其实还有一种比较常见的Content-type,当采用表单上传时,报文主体中会包含多个实体主体:

------WebKitFormBoundaryqsAGMB6Us6F7s3SF
Content-Disposition: form-data; name="image"; filename="image.png"
Content-Type: image/png


------WebKitFormBoundaryqsAGMB6Us6F7s3SF
Content-Disposition: form-data; name="text"

------WebKitFormBoundaryqsAGMB6Us6F7s3SF--

这种方式处理相对比较复杂,koa-bodyparser中并没有提供该Content-Type的解析。(下一篇中应该会介绍^_^)

五、总结

以上便是koa-bodyparser的核心实现原理,其中涉及到很多关于HTTP的基础知识,对于HTTP不太熟悉的同学,可以推荐看一波入门级宝典【图解HTTP】。

最后留图一张:

深入koa-bodyparser原理解析 

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery UI AutoComplete 使用说明
Jun 20 Javascript
javascript firefox 自动加载iframe 自动调整高宽示例
Aug 27 Javascript
js出生日期 年月日级联菜单示例代码
Jan 10 Javascript
解决Jquery向页面append新元素之后事件的绑定问题
Mar 16 Javascript
设计模式中的facade外观模式在JavaScript开发中的运用
May 18 Javascript
javascript判断firebug是否开启的方法
Nov 23 Javascript
微信小程序开发入门基础教程
Apr 19 Javascript
使用angular帮你实现拖拽的示例
Jul 05 Javascript
SVG动画vivus.js库使用小结(实例代码)
Sep 14 Javascript
如何在Vue.js中实现标签页组件详解
Jan 02 Javascript
Vue表情输入组件 微信face表情组件
Feb 11 Javascript
VUE使用draggable实现组件拖拽
Apr 06 Vue.js
jQuery实现的点击图片居中放大缩小功能示例
Jan 16 #jQuery
详解微信小程序之scroll-view的flex布局问题
Jan 16 #Javascript
vue开发环境配置跨域的方法步骤
Jan 16 #Javascript
微信小程序出现wx.getLocation再次授权问题的解决方法分析
Jan 16 #Javascript
在Create React App中启用Sass和Less的方法示例
Jan 16 #Javascript
在Create React App中使用CSS Modules的方法示例
Jan 15 #Javascript
Vue 样式绑定的实现方法
Jan 15 #Javascript
You might like
PHP脚本的10个技巧(5)
2006/10/09 PHP
php获取指定范围内最接近数的方法
2015/06/02 PHP
PHP基于PDO实现的SQLite操作类【包含增删改查及事务等操作】
2017/06/21 PHP
thinkPHP5.0框架事务处理操作简单示例
2018/09/07 PHP
Mac下关于PHP环境和扩展的安装详解
2019/10/17 PHP
仿校内登陆框,精美,给那些很厉害但是没有设计天才的程序员
2008/11/24 Javascript
JQuery 遮罩层实现(mask)实现代码
2010/01/09 Javascript
jQuery 打造动态下滑菜单实现说明
2010/04/15 Javascript
jquery获取input的value问题说明
2010/08/19 Javascript
javascript中数组array及string的方法总结
2014/11/28 Javascript
JQuery实现的购物车功能(可以减少或者添加商品并自动计算价格)
2015/01/13 Javascript
javascript实现完美拖拽效果
2015/05/06 Javascript
基于AngularJs + Bootstrap + AngularStrap相结合实现省市区联动代码
2016/05/30 Javascript
jquery通过name属性取值的简单实现方法
2016/06/20 Javascript
ES6中Iterator与for..of..遍历用法分析
2017/03/31 Javascript
原生JavaScript实现精美的淘宝轮播图效果示例【附demo源码下载】
2017/05/27 Javascript
vue2.0 自定义日期时间过滤器
2017/06/07 Javascript
Vue.js 表单控件操作小结
2018/03/29 Javascript
Vue 去除路径中的#号
2018/04/19 Javascript
详解node.js 事件循环
2020/07/22 Javascript
[02:40]2014DOTA2 国际邀请赛中国区预选赛 四大豪门抵达华西村
2014/05/23 DOTA
python实现爬虫统计学校BBS男女比例之多线程爬虫(二)
2015/12/31 Python
python实现简单爬虫功能的示例
2016/10/24 Python
Python设计模式之MVC模式简单示例
2018/01/10 Python
用Python实现大文本文件切割的方法
2019/01/12 Python
Python 类的魔法属性用法实例分析
2019/11/21 Python
用 python 进行微信好友信息分析
2020/11/28 Python
CSS3 calc()会计算属性详解
2018/02/27 HTML / CSS
CSS3改变浏览器滚动条样式
2019/01/04 HTML / CSS
AmazeUI 折叠面板的实现代码
2020/08/17 HTML / CSS
历史专业毕业生的自我鉴定
2013/11/15 职场文书
求职信内容怎么写
2014/05/26 职场文书
企业开业庆典答谢词
2015/01/20 职场文书
新年祝酒词大全
2015/08/11 职场文书
党员学习中国梦心得体会
2016/01/05 职场文书
十大动画制作软件,Adobe产品上榜两款,第一是行业标准软件
2022/03/18 杂记