Express 實戰(三):Express 基礎

BigNerdCoding發表於2017-09-16

Express 框架的初衷是為了擴充 Node 內建模組的功能提高開發效率。當你深入研究後就會發現,Express 其實是在 Node 內建的 HTTP 模組上構建了一層抽象。理論上所有 Express 實現的功能,同樣可以使用純 Node 實現。

在本文中,我們將基於前面的 Node 內容去探究 Express 和 Node 之間的關係,其中包括:中介軟體和路由等概念。當然,這裡只會進行一些綜述具體的細節會在後面帶來。

總的來說,Express 提供了 4 個主要特性:

  1. 與純 Node 中使用一個函式處理所有請求的程式碼不同, Express 則使用“中介軟體棧”處理流。
  2. 路由與中介軟體類似,只有當你通過特定 HTTP 方法訪問特定 URL 時才會觸發處理函式的呼叫。
  3. 對 request 和 response 物件方法進行了擴充。
  4. 檢視模組允許你動態渲染和改變 HTML 內容,並且使用其他語言編寫 HTML 。

中介軟體

中介軟體是 Express 中最大的特性之一。中介軟體與原生的 Node 處理函式非常類似(接受一個請求並做出響應),但是與原生不同的是,中介軟體將處理過程進行劃分,並且使用多個函式構成一個完整的處理流程。

我們將會看到中介軟體在程式碼中的各種應用。例如,首先使用一箇中介軟體記錄所有的請求,接著在其他的中介軟體中設定 HTTP 頭部資訊,然後繼續處理流程。雖然在一個“大函式”中也可以完成請求處理,但是將任務進行拆分為多個功能明確獨立的中介軟體明顯更符合軟體開發中的 SRP 規則。

中介軟體並不是 Express 特有,Python 的 Django 或者 PHP 的 Laravel 也有同樣的概念存在。同樣的 Ruby 的 Web 框架中也有被稱為 Rack 中介軟體概念。

現在我們就用 Express 中介軟體來重新實現 Hello World 應用。你將會發現只需幾行程式碼就能完成開發,在提高效率的同時還消除了一些隱藏 bug。

Express 版 Hello World

首先新建一個Express工程:新建一個資料夾並在其中新建 package.json 檔案。回想一下 package.json 的工作原理,其中完整的列出了該工程的依賴、專案名稱、作者等資訊。我們新工程中的 package.json 大致如下:

{
  "name": "hello-world",
  "author": "Your Name Here!",
  "private": true,
  "dependencies": {}
}
複製程式碼

接下來執行命令,安裝最新的 Express 並且將其儲存到 package.json 中:

npm install express -save

命令執行完成後,Express 會自動安裝到 node_modules 的檔案下,並且會在 package.json 明確列出改依賴。此時 package.json 中的內容如下:

{
  "name": "hello-world",
  "author": "Your Name Here!",
  "private": true,
  "dependencies": {
        "express": "^5.0.0"
  }
}
複製程式碼

接下來將下列程式碼複製到 app.js 中:

var express = require("express");  
var http = require("http");
var app = express();   
 
app.use(function(request, response) {  
    response.writeHead(200, { "Content-Type": "text/plain" });      
    response.end("Hello, World!");  
}); 
 
http.createServer(app).listen(3000);  

複製程式碼

首先,我們依次引入了 Express 和 HTTP 模組。

然後,使用 express() 方法建立變數 app ,該方法會返回一個請求處理函式閉包。這一點非常重要,因為它意味著我可以像之前一樣將其傳遞給 http.createServer 方法。

還記得前一章提到的原生 Node 請求處理嗎?它大致如下:

var app = http.createServer(function(request, response) {
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Hello, world!");
});
複製程式碼

兩段程式碼非常相似,回撥閉包都包含兩個引數並且響應也一樣。

最後,我們建立了一個服務並且啟動了它。http.createServer 接受的引數是一個函式,所以合理猜測 app 也只是一個函式,只不過該函式表示的是 Express 中一個完整的中介軟體處理流程。

中介軟體如何在高層工作

在原生的 Node 程式碼中,所有的 HTTP 請求處理都在一個函式中:

function requestHandler(request, response) {
    console.log("In comes a request to: " + request.url);
    response.end("Hello, world!");
}
複製程式碼

如果抽象成流程圖的話,它看起來就像:

03_01

這並不是說在處理過程中不能呼叫其它函式,而是所有的請求響應都由該函式傳送。

而中介軟體則使用一組中介軟體棧函式來處理這些請求,處理過程如下圖:

03_02

那麼,接下來我們就有必要了解 Express 使用一組中介軟體函式的緣由,以及這些函式作用。

現在我們回顧一下前面使用者驗證的例子:只有驗證通過才會展示使用者的私密資訊,與此同時每次訪問請求都要進行記錄。

在這個應用中存在三個中介軟體函式:請求記錄、使用者驗證、資訊展示。中介軟體工作流為:先記錄每個請求,然後進行使用者驗證,驗證通過進行資訊展示,最後對請求做出響應。所以,整個工作流有兩種可能情形:

03_03

另外,這些中介軟體函式中部分函式需要對響應做出響應。如果沒有做出任何響應的話,那麼伺服器會掛起請求而瀏覽器也會幹等。

這樣做的好處就是,我們可以將應用進行拆分。而拆分後的元件不僅利於後期維護,並且元件之間還可以進行不同組合。

不做任何修改的中介軟體

中介軟體函式可以對 request、response 進行修改,但它並不是必要操作。例如,前面的日誌記錄中介軟體程式碼:它只需要進行記錄操作。而一個不做任何修改,純功能性的中間函式程式碼大致如下:

function myFunMiddleware(request, response, next) {
  ...     
  nest(); 
}
複製程式碼

因為中介軟體函式的執行是從上到下的。所以,加入純功能性的請求記錄中介軟體後,程式碼如下:

var express = require("express");
var http = require("http");
var app = express();
// 日誌記錄中介軟體
app.use(function(request, response, next) {
  console.log("In comes a " + request.method + " to " + request.url);
  next();
});

// 傳送實際響應
app.use(function(request, response) {
  response.writeHead(200, { "Content-Type": "text/plain" });
  response.end("Hello, world!");
});
http.createServer(app).listen(3000);
複製程式碼

修改 request、response 的中介軟體

並不是所有的中介軟體都和上面一樣,在部分中介軟體函式需要對 request、response 進行處理,尤其是後者。

下面我們來實現前面提到的驗證中介軟體函式。為了簡單起見,這裡只允許當前分鐘數為偶數的情況通過驗證。那麼,該中介軟體函式程式碼大致如下:

app.use(function(request, response, next) {
  console.log("In comes a " + request.method + " to " + request.url);
  next();
});
app.use(function(request, response, next) {
  var minute = (new Date()).getMinutes();
  // 如果在這個小時的第一分鐘訪問,那麼呼叫next()繼續
  if ((minute % 2) === 0) {
    next();
  } else {
    // 如果沒有通過驗證,傳送一個403的狀態碼並進行響應
    response.statusCode = 403;
    response.end("Not authorized.");
  }
});
app.use(function(request, response) {
  response.end('Secret info: the password is "swordfish"!'); // 傳送密碼資訊
});
複製程式碼

第三方中介軟體類庫

在大多數情況下,你正在嘗試的工作可能已經被人實現過了。也就是說,對於一些常用的功能社群中可能已經存在成熟的解決方案了。下面,我們就來介紹一些 Express 中常用的第三方模組。

MORGAN:日誌記錄中介軟體

Morgan 是一個功能非常強大的日誌中介軟體。它能對使用者的行為和請求時間進行記錄。而這對於分析異常行為和可能的站點崩潰來說非常有用。大多數時候 Morgan 也是 Express 中日誌中介軟體的首選。

使用命令 npm install morgan --save 安裝該中介軟體,並修改 app.js 中的程式碼:

var express = require("express");
var logger = require("morgan");
var http = require("http");
var app = express();
app.use(logger("short")); 
app.use(function(request, response){
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.end("Hello, world!");
});
http.createServer(app).listen(3000);
複製程式碼

再次訪問 http://localhost:3000 你就會看到 Morgan 記錄的日誌了。

Express 的靜態檔案中介軟體

通過網路傳送靜態檔案對 Web 應用來說是一個常見的需求場景。這些資源通常包括圖片資源、CSS 檔案以及靜態 HTML 檔案。但是一個簡單的檔案傳送行為其實程式碼量很大,因為需要檢查大量的邊界情況以及效能問題的考量。而 Express 內建的 express.static 模組能最大程度簡化工作。

假設現在需要對 public 資料夾提供檔案服務,只需通過靜態檔案中介軟體我們就能極大壓縮程式碼量:

var express = require("express");
var path = require("path");
var http = require("http");
var app = express();
var publicPath = path.resolve(__dirname, "public"); 
app.use(express.static(publicPath)); 
app.use(function(request, response) {
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Looks like you didn't find a static file.");
});
http.createServer(app).listen(3000);
複製程式碼

現在,任何在 public 目錄下的靜態檔案都能直接請求了,所以你可以將所有需要的檔案的放在該目錄下。如果 public 資料夾中沒有任何匹配的檔案存在,它將繼續執行下一個中介軟體並響應一段 沒有匹配的檔案資訊。

為什麼使用 path.resolve ? 之所以不直接使用 /public 是因為 Mac 和 Linux 中目錄為 /public 而 Windows 使用萬惡的反斜槓 \public 。path.resolve 就是用來解決多平臺目錄路徑問題。

更多中介軟體

除此上面介紹的 Morgan 中介軟體和 Express 靜態中間之外,還有很多其他功能強大的中介軟體,例如:

  • connect-ratelimit:可以讓你控制每小時的連線數。如果某人向服務發起大量請求,那麼可以直接返回錯誤停止處理這些請求。
  • helmet:可以新增 HTTP 頭部資訊來應對一些網路攻擊。具體內容會在後面關於安全的章節講到。
  • cookie-parses:用於解析瀏覽器中的 cookie 資訊。
  • response-time:通過傳送 X-Response-Time 資訊,讓你能夠更好的除錯應用的效能。

路由

路由是一種將 URL 和 HTTP 方法對映到特定處理回撥函式的技術。假設工程裡有一個主頁,一個關於頁面以及一個 404 頁面,接下來看看路由是如何進行對映的:


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

// 像之前一樣設定靜態檔案中介軟體。
// 所有的請求通過這個中介軟體,如果沒有檔案被找到的話會繼續前進
var publicPath = path.resolve(__dirname, "public");
app.use(express.static(publicPath));

// 當請求根目錄的時候被呼叫
app.get("/", function(request, response) {
    response.end("Welcome to my homepage!");
});

// 當請求/about的時候被呼叫
app.get("/about", function(request, response) {
    response.end("Welcome to the about page!");
});

// 當請求/weather的時候被呼叫
app.get("/weather", function(request, response) {
    response.end("The current weather is NICE.");
});

// 前面都不匹配,則路由錯誤。返回 404 頁面
app.use(function(request, response) {
    response.statusCode = 404;
    response.end("404");
});
http.createServer(app).listen(3000);
複製程式碼

上面程式碼中除了新增前面提到的中介軟體之外,後面三個 app.get 函式就是 Express 中強大的路由系統了。它們使用 app.post 來響應一個 POST 或者 PUT 等所有網路請求。函式中第一個引數是一個路徑,例如 /about 或者 /weather 或者簡單的根目錄 / ,第二個引數是一個請求處理函式。該處理函式與之前的中介軟體工作方式一樣,唯一的區別就是呼叫時機。

除了固定路由形式外,它還可以匹配更復雜的路由(使用正則等方式):

// 指定“hello”為路由的固定部分
app.get("/hello/:who", function(request, response) {
    // :who 並不是固定住,它表示 URL 中傳遞過來的名字
    response.end("Hello, " + request.params.who + ".");
   
});
複製程式碼

重啟服務並訪問 localhost:3000/hello/earth 等到的響應資訊為:

Hello, earth

注意到如果你在 URL 後面插入多個 / 的話,例如:localhost:3000/hello/entire/earth 將會返回一個 404 錯誤。

你應該在日常生活中見過這種 URL 連結,特定的使用者能夠訪問特定的 URL 。例如,有一個使用者為 ExpressSuperHero ,那麼他的個人資訊頁面 URL 可能是:

mywebsite.com/users/Expre…

在 Express 中你可以通過這種通配方式簡化路由定義,而不必將所有使用者的特定路由都一一列舉出來。

官方文件中還展示了一個使用正規表示式來進行復雜匹配的例子,並且你可以通過路由做更多其它的事情。不過這章中只需要知道路由概念就行了,更多的內容將會在第五章中深入講解。

擴充套件 request 和 response

Express 在原來基礎上對 request 和 response 物件進行了功能擴充套件。你可以在官方文件中找到所有細節內容,不過我們可以先來領略其中的一部分:

Express 提供的功能中 redirect 算一個非常棒的功能,使用方法如下:

response.redirect("/hello/world");
response.redirect("http://expressjs.com");
複製程式碼

原生 Node 中並沒有重定向 redirect 方法。雖然我們也能夠使用原生程式碼實現重定向功能,但明顯它的程式碼量會更多。

另外,在 Express 中檔案傳送也變的更加簡單,只需一行程式碼就能實現:

response.sendFile("path/to/cool_song.mp3")
複製程式碼

與之前一樣,該功能的原生實現程式碼也比較複雜。

除了對響應物件 response 進行了擴充之外,Express 也對請求物件 request 進行了擴充。例如:你可以通過 request.ip 獲取傳送請求的機器 IP 地址或者通過 request.get 獲取 HTTP 頭部。

下面我們使用它實現 IP 黑名單功能,程式碼如下:

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

var EVIL_IP = "123.45.67.89";

app.use(function(request, response, next) {
    if (request.ip === EVIL_IP) {
        response.status(401).send("Not allowed!");
    } else {
        next();
    }
});

...
複製程式碼

這裡使用到了 req.ip 以及 res.status()res.send() ,而這些方法全都來自於 Express 的擴充。

理論上來說,我們只需要知道 Express 擴充了 request 和 response 並知道如何使用就行了,至於細節可以不去做了解。

上面的例子,只是 Express 所有擴充中的冰山一角,你可以在文件中看到更多的示例。

檢視

幾乎所有的網站內容都是基於 HTML 進行展示的,並且大多時候這些 HTML 內容都是動態生成的。你可能需要為當前登入使用者提供特定歡迎頁或者需要在頁面中動態生成資料表。為了應對動態內容的渲染,社群中出現了大量的 Express 模版引擎,例如: EJS、Handlebars、Pug。

下面是 EJS 模版引擎使用示例:

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

// 告訴 Express 你的檢視存在於一個名為 views 的資料夾中
app.set("views", path.resolve(__dirname, "views"));

// 告訴 Express 你將使用EJS模板引擎
app.set("view engine", "ejs");
複製程式碼

在程式碼中,首先我們匯入了必要的模組。然後設定了檢視檔案所在的路徑。緊接著,我們將模版引擎設定為 EJS (文件)。當然在使用 EJS 執行,我們還需要通過 npm install ejs --save 命令進行安裝。

安裝並設定好 EJS 引擎之後,接下里就是如何使用的問題了。

首先,我們在 views 資料夾下面建立一個 index.ejs 檔案,並拷貝下面的內容:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Hello, world!</title>
</head>
<body>
    <%= message %>
</body>
</html>
複製程式碼

EJS 實質上是 HTML 的一個超集,所有 HTML 的語法都可以直接使用並且完全相容。但是 EJS 對語法進行了部分擴充。 例如,你可以通過 <%= message %> 語法將傳遞過來的引數 message 插入到標籤中。

app.get("/", function(request, response) {
    response.render("index", {
        message: "Hey everyone! This is my webpage."
    });
});
複製程式碼

Express 給 response 物件新增了一個名為 render 的方法。該方法在檢視目錄下查詢第一個引數對應的模版檢視檔案並將第二個引數傳遞給該模版檔案。

下面是經過引擎渲染動態生成後的 HTML 檔案內容:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Hello, world!</title>
</head>
<body>
    Hey everyone! This is my webpage. 
</body>
</html>
複製程式碼

例項:一個留言板的實現

最後這部分,我們將會使用到前面的技術來構建一個完整的留言板 web 程式。通過這個示例來加深對上面內容的掌握,該應用主要包含兩個頁面:

  1. 一個主頁:主要用於列出之前所有的留言
  2. 一個編輯頁面:用於編輯新的留言

準備工作

首先,我們新建一個資料夾並新建專案,並複製下面內容到新建的 package.json 檔案中:

{
    "name": "express-guestbook",
    "private": true,
    "scripts": {
        "start": "node app" 
    }
}
複製程式碼

你可以在檔案中新增其他欄位資訊(例如作者或者版本),但是在本例中這並不是必要資訊。接下來,我們安裝依賴檔案,輸入命令:

npm install express morgan body-parser ejs --save

因為需要實現留言新建動作,所以這裡需要使用 body-parser 對 POST 請求進行解析。

核心程式碼

準備工作完成後,接下來就建立 app.js 檔案並複製下面的程式碼:


var http = require("http");
var path = require("path");
var express = require("express");
var logger = require('morgan');
var bodyParser = require("body-parser");

var app = express();

// 設定引擎
app.set("views", path.resolve(__dirname, "views"));
app.set("view engine", "ejs");

// 設定留言的全域性變數
var entries = [];
app.locals.entries = entries;

// 使用 Morgan 進行日誌記錄
app.use(logger("dev"));

// 設定使用者表單提交動作資訊的中介軟體,所有資訊會儲存在 req.body 裡
app.use(bodyParser.urlencoded({ extended: false }));

// 當訪問了網站根目錄,就渲染主頁(位於views/index.ejs)
app.get("/", function(request, response) {
    response.render("index");
});

// 渲染“新留言”頁面(位於views/index.ejs)當get訪問這個URL的時候
app.get("/new-entry", function(request, response) {
    response.render("new-entry");
});

// POST 動作進行留言新建的路由處理
app.post("/new-entry", function(request, response) {
    // 如果使用者提交的表單沒有標題或者內容,則返回一個 400 的錯誤
    if (!request.body.title || !request.body.body) {
        response.status(400).send("Entries must have a title and a body.");
        return;
    }
    
    // 新增新留言到 entries 中
    entries.push({
        title: request.body.title,
        content: request.body.body,
        published: new Date()
    });
    // 重定向到主頁來檢視你的新條目
    response.redirect("/");
});

// 渲染404頁面,因為你請求了未知資源
app.use(function(request, response) {
    response.status(404).render("404");
});

// 在3000埠啟動伺服器
http.createServer(app).listen(3000, function() {
    console.log("Guestbook app started on port 3000.");
});
複製程式碼

新建檢視

最後我們需要將頁面的檢視檔案補全,新建 views 資料夾,然後複製下面內容到新建 header.ejs 檔案中:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Express Guestbook</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
</head>
<body class="container">
    <h1>
        Express Guestbook
        <a href="/new-entry" class="btn btn-primary pull-right">
            Write in the guestbook
        </a>
    </h1>
    
複製程式碼

這裡使用了 Twitter 的 Bootstrap 框架,當然你也可以進行任意替換。最重要的一點是,該檔案會做為所有頁面的通用頭部。

接下來,在相同目錄下新建 footer.ejs 作為通用的 footer:

</body>
</html>
複製程式碼

通用部分完成後,接下來就是 indexnew-entry404 頁面檔案了。複製下面程式碼到檔案 views/index.ejs 中:

<% include header %>
<% if (entries.length) { %>
    <% entries.forEach(function(entry) { %>
        <div class="panel panel-default">
            <div class="panel-heading">
                <div class="text-muted pull-right">
                    <%= entry.published %>
                </div>
                <%= entry.title %>
             </div>
             <div class="panel-body">
                <%= entry.body %>
             </div>
         </div>
     <% }) %>
<% } else { %>
    No entries! <a href="/new-entry">Add one!</a>
<% } %>
<% include footer %>
複製程式碼

同時將下面的程式碼複製到 views/new-entry.ejs

<% include header %>
<h2>Write a new entry</h2>
<form method="post" role="form">
    <div class="form-group">
        <label for="title">Title</label>
        <input type="text" class="form-control" id="title" name="title" placeholder="Entry title" required>
    </div>
    <div class="form-group">
        <label for="content">Entry text</label>
        <textarea class="form-control" id="body" name="body" placeholder="Love Express! It's a great tool for building websites." rows="3" required></textarea>
    </div>
    <div class="form-group">
        <input type="submit" value="Post entry" class="btn btn-primary">
    </div>
</form>
<% include footer %>
複製程式碼

最後就是 views/404.ejs 檔案了:

<% include header %>
<h2>404! Page not found.</h2>
<% include footer %>
複製程式碼

所有的檢視檔案都建立完成了,接下來就是執行服務了。

執行服務

如果你現在就使用 npm start 拉起服務,然後訪問對應的 URL ,你就能見到下圖所示的場景了。

03_04

03_05

最後,我們回顧一下這個小專案的幾個關鍵點:

  • 使用了一箇中介軟體來記錄所有的請求,並且對不匹配的 URL 連結進行了 404 頁面響應。
  • 在新建留言後,我們將頁面重定向到了主頁。
  • 在該工程裡使用了 EJS 作為 Express 的模版引擎。並使用它實現了 HTML 檔案的動態渲染。

總結

  • Express 基於 Node 進行了工程擴充,使得開發過程更為流暢高效。
  • Express 主要有四個部分構成。
  • Express 的請求處理流程可以由多箇中介軟體進行構建。
  • Express 中流行的模版引擎為 EJS ,它能實現對 HTML 的動態渲染並且語法也更為友好。

原文地址

相關文章