
前言
Express
是基于Node.js
平台的Web
框架,应用广泛,在Express
社区中有着大量的开发者通过Express
中间件的特性,开发了各种功能的中间件,用来处理某些响应以及给请求对象req
、响应对象res
添加属性或方法,我们接下来就通过分析常用的body-parser
中间件的原理来了解如何开发Express
中间件。
body-parser 的基本使用
想分析一个中间件的原理,首先应该从使用入手,在足够了解用法的基础上去分析,现在搭建一个简易的 Express
服务,并使用 body-parser
中间件,使用前需安装。
$ npm install express body-parser
使用 body-parser
代码如下:
/* 使用 body-parser 中间件 */
const express = require('express');
const bodyParser = require('body-parser');
// 创建服务
const app = express();
// 使用 body-parser 中间
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// 创建路由
app.post('/login', function (req, res) {
console.log(req.body);
res.send(req.body);
});
// 监听服务
app.listen(3000, function () {
console.log('server start 3000');
});
启动上面的服务器,通过 postman
工具分别通过表单提交和 json
的格式访问 http://localhost:3000/login,查看服务器控制后台的打印结果和 postman
的返回结果。
body-parser 的实现
原理分析
从上面的使用案例我们可以分析出一下几点:
- 首先,
body-parser
中间件的作用是给req
添加属性body
,值为对象,以键值对的形式存储请求体中的参数;- 其次,
body-parser
只处理POST
请求;- 最后,
body-parser
模块导出一个对象,上面有两个方法urlencoded
和json
,分别处理表单提交和json
格式的请求体参数。
分析 urlencoded、json 公共逻辑
在实现之前我们先分析一下两个方法:
- 首先都需要先读取请求体中的内容,数据传输的类型为
Buffer
,转换成字符串后会根据提交方式不同而导致请求体中的内容是查询字符串或者是json
字符串的区别;- 当解析失败时都需要做错误处理;
- 当不是
POST
请求时都需要向下执行其他中间件;- 而最核心的事就是把请求体中的数据转换成对象挂在
req.body
上。
使用的转换数据的方法不同是唯一的区别,能区分两者的就是请求头 Content-Type
的值,因此我们可以把所有的公共逻辑抽取出来用一个 acceptPost
函数来执行。
模块的创建
我们下面创建自己的 body-parser
模块,防止命名冲突,模块命名为 my-body-parser
,处理参数需要使用 querystring
和 qs
两个模块,其中 qs
是第三方模块,使用前需安装。
$ npm install qs
qs
和 querystring
作用基本相同,就是处理查询字符串格式的参数,但是也有一点小小的区别,querystring
只能处理一级,而 qs
可以处理多级。
/* 文件:my-body-parser.js */
const querystring = require('querystring');
const qs = require('qs');
// urlencoded 和 json 公共逻辑
function acceptPost() {
// ...
}
// 处理表单提交的方法
function urlencoded() {
// ...
}
// 处理请求体 json 的方法
function json() {
// ...
}
// 导出对象
module.exports = { urlencoded, json };
在把基本模块搭建好后,我们下面就实现 body-parser
模块内的公共逻辑函数 acceptPost
。
acceptPost 的实现
为了兼容 urlencoded
方法和 json
方法设计了两个参数,一个是区分当前调用方法的 type
,一个是针对 urlencoded
方法的 options
。
/* acceptPost 的实现 */
// urlencoded 方法和 json 方法的公共逻辑函数
function acceptPost(type, options) {
// 返回一个中间件函数
return function (req, res, next) {
// 获取请求头
const contentType = req.headers['content-type'];
// 判断如果不符合两种提交的请求头直接交给其他中间件处理
if (
contentType === 'application/x-www-form-urlencoded' ||
contentType === 'application/json'
) {
// 存储数据的数组
const buffers = [];
req.on('data', function (data) {
// 接收数据并存入数组中
buffers.push(data);
});
req.on('end', function () {
// 组合数据并转换成字符串
const result = Buffer.concat(buffers).toString();
// 处理数据并挂载 req.body 属性上
// 如果是表单提交则使用 querystring 或 qs,否则使用 JSON.parse
if (type === 'form') {
// 如果配置 extended 值为 true 使用 qs,否则使用 querystring
req.body = options.extended ? qs.parse(result) : querystring.parse(result);
} else if (type === 'json') {
req.body = JSON.parse(result);
}
next(); // 向下执行
});
// 错误处理
req.on('err', function (err) {
next(err);
});
} else {
next();
}
}
}
urlencoded 和 json 方法的实现
// 处理表单提交的方法
function urlencoded(options) {
// 定义 type 值
const type = 'form';
return acceptPost(type, options)
}
// 处理请求体 json 的方法
function json() {
// 定义 type 值
const type = 'json';
return acceptPost(type);
}
当我们把所有的公共逻辑都抽取出去后发现,urlencoded
和 json
方法内部只需要定义不同的类型就可以执行自己的中间件逻辑。
总结
上面分析
body-parse
中间件的原理的目的在于理解Express
中间件开发的模式,在此总结一下,Express
中间件返回的是一个函数,形参为req
、res
和next
,当功能无法处理某些情况时需要调用next
,当出现错误时调用next
并传递错误,则交给Express
内置的错误处理中间件,在中间件内部代码涉及异步操作时,须在异步完成的回调当中调用next
,这是不如Koa
方便的一点,同时也是两者的区别,因为Koa
中已经大量使用async/await
,在执行异步代码时可以等待。