前言

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 模块导出一个对象,上面有两个方法 urlencodedjson,分别处理表单提交和 json 格式的请求体参数。

分析 urlencoded、json 公共逻辑

在实现之前我们先分析一下两个方法:

  • 首先都需要先读取请求体中的内容,数据传输的类型为 Buffer,转换成字符串后会根据提交方式不同而导致请求体中的内容是查询字符串或者是 json 字符串的区别;
  • 当解析失败时都需要做错误处理;
  • 当不是 POST 请求时都需要向下执行其他中间件;
  • 而最核心的事就是把请求体中的数据转换成对象挂在 req.body 上。

使用的转换数据的方法不同是唯一的区别,能区分两者的就是请求头 Content-Type 的值,因此我们可以把所有的公共逻辑抽取出来用一个 acceptPost 函数来执行。

模块的创建

我们下面创建自己的 body-parser 模块,防止命名冲突,模块命名为 my-body-parser,处理参数需要使用 querystringqs 两个模块,其中 qs 是第三方模块,使用前需安装。

$ npm install qs

qsquerystring 作用基本相同,就是处理查询字符串格式的参数,但是也有一点小小的区别,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);
}

当我们把所有的公共逻辑都抽取出去后发现,urlencodedjson 方法内部只需要定义不同的类型就可以执行自己的中间件逻辑。

总结

上面分析 body-parse 中间件的原理的目的在于理解 Express 中间件开发的模式,在此总结一下,Express 中间件返回的是一个函数,形参为 reqresnext,当功能无法处理某些情况时需要调用 next,当出现错误时调用 next 并传递错误,则交给 Express 内置的错误处理中间件,在中间件内部代码涉及异步操作时,须在异步完成的回调当中调用 next,这是不如 Koa 方便的一点,同时也是两者的区别,因为 Koa 中已经大量使用 async/await,在执行异步代码时可以等待。