Express 實戰(四):中介軟體

BigNerdCoding發表於2019-03-04

原生 Node 的單一請求處理函式,隨著功能的擴張勢必會變的越來越難以維護。而 Express 框架則可以通過中介軟體的方式按照模組和功能對處理函式進行切割處理。這樣拆分後的模組不僅邏輯清晰,更重要的是對後期維護和開發非常有利。

本文將會詳細介紹 Express 的使用,其中主要內容包括:

  • 中介軟體是什麼?
  • 中介軟體棧以及請求處理的工作流。
  • 中介軟體的使用。
  • 如何實現自己的中介軟體。
  • Express 中常用的第三方中介軟體。

希望在讀完本文後,你能對這個 Express 最主要的構成有更加清晰的認知。

中介軟體和中介軟體棧

對所有的 Web 應用來說它的處理流程可以簡單描述為:監聽請求、解析請求、做出響應。當然,Node 也遵循這一套流程,只不過將那些請求都轉化為了 JavaScript 物件。

04_01
04_01

與原生 Node 程式碼不同的是,Express 會將上圖中的最後一部分拆分為一組中介軟體函式(中介軟體棧)。所以Express 的工作流大致如下:

04_02
04_02

與純 Node 不同的是,Express 中的中介軟體棧函式中除了表示請求和響應的引數外,還新增了第三個引數。該引數是一個函式物件,按照慣例我們稱之為 next 。它用於傳遞中介軟體棧對某個請求的處理流。

04_03
04_03

在整個中介軟體棧的處理流中,最少有一個函式需要呼叫 res.end 方法結束響應處理。下面我們就通過搭建靜態檔案服務來加深對中介軟體棧的理解。

示例:一個靜態檔案伺服器

建立一個資料夾併為此提供靜態檔案服務。你可以在資料夾中存放任何檔案,例如:HTML 檔案、圖片。最終所有的這些檔案都能通過示例程式進行網路訪問。

該示例程式的功能大致包括:能夠正確返回存在的檔案;檔案不存在時返回 404 錯誤;列印所有的訪問請求。所以,該示例的中介軟體棧如下:

  1. 日誌記錄中介軟體。該函式會在終端列印所有的網路請求,並在列印介紹後繼續下一個中介軟體函式。
  2. 靜態檔案傳送中介軟體。如果訪問的檔案存在則返回給客戶端。如果檔案不存在則會跳到錯誤處理中介軟體。
  3. 404 處理中介軟體。如果檔案不存在的話,該中介軟體將會給客戶端傳送 404 錯誤資訊。

流程圖如下:

04_04
04_04

明確示例的目標和需求後,下面我們就進行程式碼實現。

準備工作

與之前一樣,新建工程目錄並將下面內容複製到 package.json 中:

{
    "name": "static-file-fun", 
    "private": true, 
    "scripts": {
        "start": "node app.js" 
    }
}複製程式碼

接下來,我們執行 npm install express --save 安裝最新版 Express 。確保安裝完成後,我們在工程目錄裡新建資料夾 static 並在其中存放一些檔案。最後,我們新建工程主入口檔案 app.js 。一切就緒後,工程的大致目錄如下:

04_05
04_05

另外,值的一提的是之所以配置 npm start 命令,既是因為開發約定更重要的是讓其他人開箱即用無需自己手動查詢程式入口。

第一個中介軟體:日誌記錄

按照前面制訂的處理流程,首先需要實現的就是日誌中介軟體。複製下面程式碼到入口檔案 app.js 中:

var express = require("express");
var path = require("path");
var fs = require("fs");

var app = express();

app.use(function(req, res, next) {
    console.log("Request IP: " + req.url);
    console.log("Request date: " + new Date());
});

app.listen(3000, function() {
    console.log("App started on port 3000");
});複製程式碼

通過上面的 app.use 函式,我們成功實現了應用中的第一個功能,即記錄每次網路請求。當然這裡還有一個問題,當前應用並不會對請求做出響應。這意味這:如果你用 npm start 拉起服務並訪問 loaclhost:3000 瀏覽器會一直掛起等待響應直到出現超時錯誤。不過不要擔心,等補全後面功能後我們就可以在該中介軟體呼叫 next() 將響應的任務交給後續中介軟體。

這裡我們只需要明白:理論上一個中介軟體函式處理結束後,它必須執行以下兩個步驟中的一個。

  1. 所有處理結束,傳送 red.end 或者 Express 中的 red.sendFile 等函式結束響應。
  2. 呼叫 next 函式執行下一個中介軟體函式。

所以這裡我們先把 next() 呼叫補全將日誌中介軟體的邏輯理順:

// ...
app.use(function(req, res, next) {
    console.log("Request IP: " + req.url);
    console.log("Request date: " + new Date());
    next(); // 新的這行很重要
});
// ...複製程式碼

此時重啟服務並訪問 http://localhost:3000 的話訪問請求會被完整記錄下來。但是因為程式沒有做出響應 ,Express 任會給客戶端傳送一個錯誤資訊。所以,接下來我們就補全後續流程。

靜態檔案服務中介軟體

靜態檔案服務中介軟體應該有以下幾個功能:

  1. 檢查目錄中是否存在該檔案
  2. 如果檔案存在則呼叫 res.sendFile 結束響應處理。
  3. 如果檔案不存在則繼續呼叫下一個中介軟體從程式碼角度來說就是呼叫 next

其中我們需要使用內建的 path 模組指定路徑,然後使用內建的 fs 模組判斷檔案釋放存在。將下面程式碼新增到日誌中介軟體後面:


// 日誌中介軟體
app.use(function(req, res, next) {
  // …
});

app.use(function(req, res, next) {
  var filePath = path.join(__dirname, "static", req.url);  
  fs.exists(filePath, function(exists) {                      
    if (exists) {                                             
      res.sendFile(filePath);                                 
    } else {                                                  
      next();                                                
    }
  });
});

app.listen(3000, function() {
    ...
}複製程式碼

在中介軟體中我們首先使用 path.join 拼接檔案完整路徑。例如,如果使用者訪問 http://localhost:3000/celine.mp3 檔案的話 req.url 的值就是 /celine.mp3 拼接後的完整路徑就是 "/path/to/your/project/static/celine.mp3" 了。

然後,該中介軟體呼叫 fs.exists 函式檢查檔案是否存在。如果檔案存在則發生檔案,否則呼叫 next() 繼續執行下一個中介軟體。而如果訪問的 URL 沒有對應的檔案的話就會出現之前一樣的錯誤。所以下面需要實現最後一箇中介軟體:404 處理中介軟體。

404 處理中介軟體

404 中介軟體的任務就是傳送 404 錯誤資訊,複製下面的實現程式碼並新增到靜態服務中介軟體後面:



app.use(function(req, res) {
    // 設定狀態碼為404
    res.status(404);
    // 傳送錯誤提示
    res.send("File not found!");
});

// ...複製程式碼

這樣整個工程就算完成了。如果你再次啟動服務的話,之前的錯誤就會被一個 404 錯誤取代。另外,如果你將該中介軟體函式移動到中介軟體棧的第一個,那麼你會發現所有的請求都會得到 404 的錯誤資訊。這意味著中介軟體棧中的函式順序是非常重要的。

到這裡,app.js 中的完整程式碼如下:


var express = require("express");
var path = require("path");
var fs = require("fs");
var app = express();
app.use(function(req, res, next) {
    console.log("Request IP: " + req.url);
    console.log("Request date: " + new Date());
    next();
});
app.use(function(req, res, next) {
    var filePath = path.join(__dirname, "static", req.url);
    fs.stat(filePath, function(err, fileInfo) {
        if (err) {
            next();
            return;
        }
        if (fileInfo.isFile()) {
            res.sendFile(filePath);
        } else {
            next();
        }
    });
});
app.use(function(req, res) {
    res.status(404);
    res.send("File not found!");
});
app.listen(3000, function() {
    console.log("App started on port 3000");
});複製程式碼

當然,這只是初步的程式碼,還有很多地方可以進行優化。

將日誌中介軟體替換為:Morgan

在軟體開發中如果你的問題已經存在比較好的解決方案,那麼理想的做法是直接使用該方案而不應該“重複造輪子”。所以,下面我們使用功能強大的 Morgan 替換掉上面自己實現的日誌中介軟體。雖然,該中介軟體不是 Express 內建模組,但是它卻是由 Express 團隊維護並久經考驗。

執行 npm install morgan --save 安裝最新版本的 Morgan 模組。然後使用 Morgan 替換掉之前的日誌中介軟體:

var express = require("express");
var morgan = require("morgan");
...

var app = express();
app.use(morgan("short"));

...複製程式碼

當你再次啟動服務並訪問資源時,終端將會列印包括 IP 地址在內的有用資訊:

04_06
04_06

程式碼中 morgan 其是一個函式並且它的返回值是一箇中介軟體函式。當你呼叫它的時候,它會返回一個類似之間實現的日誌中介軟體。為了程式碼更加清晰,你也可以將程式碼改寫為:

var morganMiddleware = morgan("short");
app.use(morganMiddleware);複製程式碼

另外,這裡在呼叫函式是使用的是 short 作為輸出選項。其實該模組還提供另兩個輸出選項:combined 列印最多資訊;tiny 列印最少的資訊。

除了使用 Morgan 替換原有日誌中介軟體之外,我們還可以使用內建的靜態中介軟體替換之前的程式碼實現。

使用 Express 內建靜態檔案中介軟體

接下來,我們使用 Express 內建的 express.static 模組來替換之前的靜態檔案中介軟體。它的工作原理與之前的中介軟體程式碼類似,但是它具有更好的安全性和效能。例如,它在內部實現了資源的快取功能。

與 Morgan 一樣,express.static 函式的返回值也是一箇中介軟體函式。我們只需為 express.static 函式指定路徑引數即可。程式碼如下:

var staticPath = path.join(__dirname, "static"); // 設定靜態檔案的路徑
app.use(express.static(staticPath)); // 使用express.static從靜態路徑提供服務
// ...複製程式碼

完成替換後你會發現程式碼相較之前明顯變的簡練了,與此同時功能反而比之前更強。另外,這些久經考驗的中介軟體模組遠比自己的程式碼實現功能更多也更可靠。此時 app.js 中的完整程式碼:

var express = require("express");
var morgan = require("morgan");
var path = require("path");
var app = express();
app.use(morgan("short"));
var staticPath = path.join(__dirname, "static");
app.use(express.static(staticPath));
app.use(function(req, res) {
    res.status(404);
    res.send("File not found!");
});
app.listen(3000, function() {
    console.log("App started on port 3000");
});複製程式碼

錯誤處理中介軟體

之前我說過呼叫 next() 會按序執行下一個中介軟體。其實,真實情況並不是這麼簡單。事實上,Express 中介軟體有兩種型別。

到目前為止,你已經接觸了第一種型別:包含三個引數的常規中介軟體函式(有時 next 會被忽略而只保留兩個引數),而絕大多數時候程式中都是使用這種常規模式。

第二種型別非常少見:錯誤處理中介軟體。當你的 app 處於錯誤模式時,所有的常規中介軟體都會被跳過而直接執行 Express 錯誤處理中介軟體。想要進入錯誤模式,只需在呼叫 next 時附帶一個引數。這是呼叫錯誤物件的一種慣例,例如:next(new Error("Something bad happened!"))

錯誤處理中介軟體中需要四個引數,其中後面三個和常規形式的一致而第一個引數則是 next(new Error("Something bad happened!")) 中傳遞過來的 Error 物件。你可以像使用常規中介軟體一樣來使用錯誤處理中介軟體,例如:呼叫 res.end 或者 next 。如果呼叫含引數的 next 中介軟體會繼續下一個錯誤處理中介軟體否則將會退出錯誤模式並呼叫下一個常規中介軟體。

假設,現在有四個中介軟體依次排開,其中第三個為錯誤處理中介軟體而其他的都是常規中介軟體。如果沒有出現錯誤的話,流程應該是:

04_07
04_07

如上所示,當沒有錯誤發生時錯誤處理中介軟體就像不存在一樣。但是,一旦出現錯誤所有的常規中介軟體都被跳過,那麼處理流程就會是這樣:

04_08
04_08

雖然 Express 沒有做出強制規定,但是一般錯誤處理中介軟體都會放在中介軟體棧的最下面。這樣所有之前的常規中介軟體發生錯誤時都會被該錯誤處理中介軟體所捕獲。

Express 的錯誤處理中介軟體只會捕獲由 next 觸發的錯誤,對於 throw 關鍵字觸發的異常則不在處理範圍內。對於這些異常 Express 有自己的保護機制,當請求失敗時 app 會返回一個 500 錯誤並且整個服務依舊在持續執行。然而,對於語法錯誤這類異常將會直接導致服務奔潰。

現在通過一個簡單示例來看看 Express 中的錯誤處理中介軟體。假設該應用對於使用者的任何請求都是通過 res.sendFile 發生圖片給使用者。程式碼如下:

var express = require("express");
var path = require("path");
var app = express();

var filePath = path.join(__dirname, "celine.jpg");
app.use(function(req, res) {
  res.sendFile(filePath);
});
app.listen(3000, function() {
  console.log("App started on port 3000");
});複製程式碼

可以看到這是之前靜態檔案服務的簡化版,對於任意請求都會發生 celine.jpg 圖片。但是如果該檔案不存在,或者是檔案讀取過程發生了錯誤該怎麼辦呢?這就需要一些機制來處理這種異常錯誤了,而這正是錯誤處理中介軟體存在的理由。

為了觸發異常處理,我們在 res.sendFile 將異常回撥函式補充完整。這個回撥函式將會在檔案傳送之後得到執行並且該回撥函式中有一個引數標記檔案傳送成功與否。程式碼示例如下:

res.sendFile(filePath, function(err) {
  if (err) {
    console.error("File failed to send.");
  } else {
    console.log("File sent!");
  }
});複製程式碼

當然,除了列印錯誤資訊之外,我們還可以通過觸發異常進入錯誤處理中介軟體函式,而該部分程式碼實現如下:

// ...
app.use(function(req, res, next) {
  res.sendFile(filePath, function(err) {
    if (err) {
      next(new Error("Error sending file!"));
    }
  });
});
// ...複製程式碼

異常觸發後接下來就是錯誤處理中介軟體的實現了。

通常情況下我們都會首先將錯誤資訊記錄下來,而這些資訊一般也不會展示給使用者。畢竟將一長段的 JavaScript 棧呼叫資訊展示給不懂技術的使用者會給他們造成不必要的困惑。尤其是這些資訊一旦暴露給了黑客,他們有可能就能逆向分析出網站是如何工作的從而造成資訊風險。

下面,我們僅僅在處理處理中介軟體中列印錯誤資訊而不做任何進一步的處理。它與之前的中介軟體類似只不過這裡列印錯誤資訊而不是請求資訊。將下面程式碼複製到所有常規中介軟體的後面:

// ...

app.use(function(err, req, res, next) {
    // 記錄錯誤
    console.error(err);
    // 繼續到下一個錯誤處理中介軟體
    next(err);
});
// ...複製程式碼

現在,當程式出現異常之後這些錯誤資訊都將會被記錄在控制檯以便後面的進一步分析。當然,這裡還有一些事情需要處理,例如:對請求作出響應。將下面程式碼放在上一個中介軟體之後:

// ...

app.use(function(err, req, res, next) {
  // 設定狀態碼為500
  res.status(500);
  // 傳送錯誤資訊
  res.send("Internal server error.");
});
// ...複製程式碼

請記住,這些錯誤處理中介軟體不管所在位置如何它都只能通過帶參 next 進行觸發。對於這個簡單應用來說可能沒有那麼多異常和錯誤會觸發錯誤處理中介軟體。但是隨著應用的擴張,你就需要對錯誤行為進行仔細測試。如果發生了異常,那麼你應該對妥善的處理好這些異常而不是讓程式崩潰。

總結

在本文中我們仔細探討了 Express 的核心模組:中介軟體。其中的內容包括:

  • Express 中介軟體棧的概念以及工作流。
  • 如何編寫自定義的中介軟體函式。
  • 如何編寫錯誤處理中介軟體。
  • 常見中介軟體模組的使用。

原文地址

相關文章