原文出自:https://www.pandashen.com
前言
Express
是基於 NodeJS 平臺的 Web 框架,應用廣泛,在 Express
社區中有着大量的開發者通過 Express
中間件的特性,開發了各種功能的中間件,用來處理某些響應以及給請求對象 req
、響應對象 res
添加屬性或方法,我們接下來就通過分析常用的 body-parser
中間件的原理來了解如何開發 Express
中間件,如果想了解更多 Express
內部封裝原理可以看 《Express 源碼分析及簡易封裝》。
body-parser 的基本使用
想刨析一箇中間件的原理,首先應該從使用入手,在足夠了解用法的基礎上去分析,現在搭建一個簡易的 Express
服務,並使用 body-parser
中間件,使用前需安裝。
npm install express 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 的實現
1、原理分析
從上面的使用案例我們可以分析出一下幾點:
- 首先,
body-parser
中間件的作用是給req
添加屬性body
,值爲對象,以鍵值對的形式存儲請求體中的參數; - 其次,
body-parser
只處理POST
請求; - 最後,
body-parser
模塊導出一個對象,上面有兩個方法urlencoded
和json
,分別處理表單提交和json
格式的請求體參數。
2、分析 urlencoded、json 公共邏輯
在實現之前我們先分析一下兩個方法,首先都需要先讀取請求體中的內容,數據傳輸的類型爲 Buffer,轉換成字符串後會根據提交方式不同而導致請求體中的內容是查詢字符串或者是 json
字符串的區別。
當解析失敗時都需要做錯誤處理,當不是 POST
請求時都需要向下執行其他中間件,而最核心的事就是把請求體中的數據轉換成對象掛在 req.body
上。
使用的轉換數據的方法不同是唯一的區別,能區分兩者的就是請求頭 Content-Type
的值,因此我們可以把所有的公共邏輯抽取出來用一個 acceptPost
函數來執行。
3、模塊的創建
我們下面創建自己的 body-parser
模塊,防止命名衝突,我們的模塊命名爲 my-body-parser
,處理參數需要使用 querystring
和 qs
兩個模塊,其中 qs
是第三方模塊,使用前需安裝。
npm install qs
qs
和 querystring
作用基本相同,就是處理查詢字符串格式的參數,但是也有一點小小的區別,querystring
只能處理一級,而 qs
可以處理多級。
const querystring = require("querystring");
const qs = require("qs");
// urlencoded 和 json 公共邏輯
function acceptPost() {
// ...
}
// 處理表單提交的方法
function urlencoded() {
// ...
}
// 處理請求體 json 的方法
function json() {
// ...
}
// 導出對象
module.exports = { urlencoded, json };
在把基本模塊搭建好後,我們下面就實現 body-parser
模塊內的公共邏輯函數 acceptPost
。
4、acceptPost 的實現
爲了兼容 urlencoded
方法和 json
方法設計了兩個參數,一個是區分當前調用方法的 type
,一個是針對 urlencoded
方法的 options
。
// acceptPost 的實現
// urlencoded 方法和 json 方法的公共邏輯函數
function acceptPost(type, options) {
// 返回一箇中間件函數
return function (req, res, next) {
// 獲取請求頭
let contentType = req.headers["content-type"];
// 判斷如果不符合兩種提交的請求頭直接交給其他中間件處理
if (
contentType === "application/x-www-form-urlencoded" ||
contentType === "application/json"
) {
// 存儲數據的數組
let buffers = [];
req.on("data", function (data) {
// 接收數據並存入數組中
buffers.push(data);
});
req.on("end", function () {
// 組合數據並轉換成字符串
let 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();
}
}
}
5、urlencoded 和 json 方法的實現
// 處理表單提交的方法
function urlencoded(options) {
// 定義 type 值
let type = "form";
return acceptPost(type, options)
}
// 處理請求體 json 的方法
function json() {
// 定義 type 值
let type = "json";
return acceptPost(type);
}
當我們把所有的公共邏輯都抽取出去後發現,urlencoded
和 json
方法內部只需要定義不同的類型就可以執行自己的中間件邏輯。
總結
上面分析 body-parse
中間件的原理的目的在於理解 Express
中間件開發的模式,在此總結一下,Express
中間件返回的是一個函數,形參爲 req
、res
和 next
,當功能無法處理某些情況時需要調用 next
,當出現錯誤時調用 next
並傳遞錯誤,則交給 Express
內置的錯誤處理中間件,在中間件內部代碼涉及異步操作時,須在異步完成的回調當中調用 next
,這是不如 Koa
方便的一點,同時也是兩者的區別,因爲 Koa
中已經大量使用 async/await
,在執行異步代碼時可以等待。