Express 實戰(四):中間件

原生 Node 的單一請求處理函數,隨着功能的擴張勢必會變的愈來愈難以維護。而 Express 框架則能夠經過中間件的方式按照模塊和功能對處理函數進行切割處理。這樣拆分後的模塊不只邏輯清晰,更重要的是對後期維護和開發很是有利。html

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

  • 中間件是什麼?
  • 中間件棧以及請求處理的工做流。
  • 中間件的使用。
  • 如何實現本身的中間件。
  • Express 中經常使用的第三方中間件。

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

中間件和中間件棧

對全部的 Web 應用來講它的處理流程能夠簡單描述爲:監聽請求、解析請求、作出響應。固然,Node 也遵循這一套流程,只不過將那些請求都轉化爲了 JavaScript 對象。npm

04_01
04_01

與原生 Node 代碼不一樣的是,Express 會將上圖中的最後一部分拆分爲一組中間件函數(中間件棧)。因此Express 的工做流大體以下:json

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 中間件棧的概念以及工做流。
  • 如何編寫自定義的中間件函數。
  • 如何編寫錯誤處理中間件。
  • 常見中間件模塊的使用。

原文地址

相關文章
相關標籤/搜索