当前位置: 首页 > 工具软件 > koa-hbs > 使用案例 >

koa源码部分解析(3)koa-body

薛阳荣
2023-12-01

版本4.1.1

惯例,先来看看简单的使用

const Koa = require('koa');
const body = require('koa-body');

const app = new Koa();

// 可以给parsedMethods传想要koa-body解析的方法
app.use(body({parsedMethods: ['POST', 'PUT', 'PATCH', 'GET', 'HEAD', 'DELETE']}));

app.use(async function(ctx) {
  ctx.body = '123123';
});

app.listen(3001);

koa-body自己的代码其实没几行,先处理一堆参数(大多是传给co-body和formidable用的),然后用type-is这个包(ctx.is函数)判断出请求的数据类型,然后根据不同类型用co-body和formidable来解析,拿到解析结果以后放到body或者files里面,齐活。

type-is

所以我们先来看看type-is是什么神奇的包,虽然写的挺复杂而且引了mime-types和media-typer,但是最终起作用的还是mime-db这个包,看名字就知道其实存了很多很多文件类型,总之type-is就是去匹配了下请求类型是什么(通过content-type来判断,而不是请求内容),来贴一段koa-body的代码,我们也可以用ctx.is来判断请求的类型,就抄下面的写法就行

const jsonTypes = [
  'application/json',
  'application/json-patch+json',
  'application/vnd.api+json',
  'application/csp-report'
];

// 省略一些代码

if (opts.json && ctx.is(jsonTypes)) {
  bodyPromise = buddy.json(ctx, {
    encoding: opts.encoding,
    limit: opts.jsonLimit,
    strict: opts.jsonStrict,
    returnRawBody: opts.includeUnparsed
  });
} else if (opts.urlencoded && ctx.is('urlencoded')) {
	// 对应application/x-www-form-urlencoded
  bodyPromise = buddy.form(ctx, {
    encoding: opts.encoding,
    limit: opts.formLimit,
    queryString: opts.queryString,
    returnRawBody: opts.includeUnparsed
  });
} else if (opts.text && ctx.is('text')) {
  bodyPromise = buddy.text(ctx, {
    encoding: opts.encoding,
    limit: opts.textLimit,
    returnRawBody: opts.includeUnparsed
  });
} else if (opts.multipart && ctx.is('multipart')) {
	// 用formidable来解析
  bodyPromise = formy(ctx, opts.formidable);
}

// 省略一些代码
bodyPromise = bodyPromise || Promise.resolve({});

在type-is里有一个函数叫hasbody,get请求在这个函数的判断下被认为没有body,所以get请求获取到的结果都是空对象,这个空对象来自上面代码段中的最后一句,Promise.resolve({})。

function hasbody (req) {
  return req.headers['transfer-encoding'] !== undefined ||
    !isNaN(req.headers['content-length'])
}

function typeofrequest (req, types_) {
  var types = types_

  // no body
  if (!hasbody(req)) {
    return null
  }
  
  // 省略一些代码
}

关于is函数的使用方法及输出结果,koa本体的代码注释里也有相对详细的介绍,在request里面。

co-body

接下来我们来看co-body,代码结构很简单,提供了json、form、text这三种格式的解析,主要依赖的是inflation和raw-body,先贴一段text的,最简单,json和form也差不多,处理了一下参数(主要是encoding和limit参数),用inflation和raw-body解析了一把,返回,和koa-body一毛一样

module.exports = function(req, opts){
  req = req.req || req;
  opts = utils.clone(opts);

  // defaults
  var len = req.headers['content-length'];
  var encoding = req.headers['content-encoding'] || 'identity';
  if (len && encoding === 'identity') opts.length = ~~len;
  opts.encoding = opts.encoding === undefined ? 'utf8': opts.encoding;
  opts.limit = opts.limit || '1mb';

  // raw-body returns a Promise when no callback is specified
  return Promise.resolve()
    .then(function() {
    	// 关键的一句
      return raw(inflate(req), opts);
    })
    .then(str => {
      // ensure return the same format with json / form
      return opts.returnRawBody ? { parsed: str, raw: str } : str;
    });
};

于是我们再来看inflation和raw-body是怎么回事,inflation比较简单,根据content-encoding的类型,做不同的操作,如果是gzip和deflate,调用zlib.Unzip解压缩,如果是identity,直接返回输入值。

至于raw-body,它做的事情是stream解析,关于stream的细节可以看看这篇https://blog.csdn.net/u011393161/article/details/104472698,还瞅见一个挺吓人的包,iconv-lite,有兴趣可以看看。

formidable

如果我们的数据类型是被type-is判断为multipart,那么就会调用formidable来进行解析,formidable本身也提供了很多种格式的解析,有json,multipart,urlencoded等,所以实际上co-body和formidable有点重叠,感觉上有formidable就够用了。

结构上formidable也是提供了各种格式各自的解析实现,主要是write和end两个函数,因为multipart格式的解析写的比较复杂,所以我们可以先看看json格式或者urlencoded格式的解析,不过要将koa-body的代码略做修改,不然这两种格式是由co-body来解析的,不会进入formidable,这里就不贴修改的代码了,很简单。

我们先来看看json的解析,formidable是通过下面这样的正则对content-type进行判断来决定解析格式的,和koa-body的判断方法不一致,有点尴尬

this.headers['content-type'].match(/json/i)
this.headers['content-type'].match(/urlencoded/i)
this.headers['content-type'].match(/multipart/i)
this.headers['content-type'].match(/octet-stream/i) // 这种格式不太常见。。。吧

formidable的parse函数的主要流程是先在writeHeaders函数中调用_parseContentType,初始化对应格式的解析器,然后开始注册stream的事件,当data事件注册,stream进入流动模式,解析器中的write函数就开始执行,解析过程中还有一些自定义的事件,可以进行订阅处理,koa-body中订阅了error、field、file等事件,其实也可以通过回调函数来获取解析的fields和files。

json格式的解析比较简单,往chunks里不断写数据就是了,multipart就比较复杂,构造一个multipart的数据都挺麻烦了,我也是第一次注意到multipart的content-type其实是像这样的

multipart/form-data; boundary=--------------------------010958153996667037724890

form-data这个包可以在node端模拟FormData的数据,multipart_parser是根据传输的数据格式一块一块的分开来解析,可以看到multipart_parser里面有个大的switch,里面标记了各个部分,具体细节不展开了

结语

感觉比较细节的实现都没讲到,实在惭愧,不过一来自己没有理的很清楚,二来是很多处理是流和buffer,不像普通的数据那么容易看明白,哪天有空了回头再来补一补

 类似资料: