Express 實戰(三):Express 基礎

Express 框架的初衷是爲了拓展 Node 內置模塊的功能提升開發效率。當你深刻研究後就會發現,Express 實際上是在 Node 內置的 HTTP 模塊上構建了一層抽象。理論上全部 Express 實現的功能,一樣可使用純 Node 實現。css

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

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

  1. 與純 Node 中使用一個函數處理全部請求的代碼不一樣, Express 則使用「中間件棧」處理流。
  2. 路由與中間件相似,只有當你經過特定 HTTP 方法訪問特定 URL 時纔會觸發處理函數的調用。
  3. 對 request 和 response 對象方法進行了拓展。
  4. 視圖模塊容許你動態渲染和改變 HTML 內容,而且使用其餘語言編寫 HTML 。

中間件

中間件是 Express 中最大的特性之一。中間件與原生的 Node 處理函數很是相似(接受一個請求並作出響應),可是與原生不一樣的是,中間件將處理過程進行劃分,而且使用多個函數構成一個完整的處理流程。git

咱們將會看到中間件在代碼中的各類應用。例如,首先使用一箇中間件記錄全部的請求,接着在其餘的中間件中設置 HTTP 頭部信息,而後繼續處理流程。雖然在一個「大函數」中也能夠完成請求處理,可是將任務進行拆分爲多個功能明確獨立的中間件明顯更符合軟件開發中的 SRP 規則。github

中間件並非 Express 特有,Python 的 Django 或者 PHP 的 Laravel 也有一樣的概念存在。一樣的 Ruby 的 Web 框架中也有被稱爲 Rack 中間件概念。web

如今咱們就用 Express 中間件來從新實現 Hello World 應用。你將會發現只需幾行代碼就能完成開發,在提升效率的同時還消除了一些隱藏 bug。正則表達式

Express 版 Hello World

首先新建一個Express工程:新建一個文件夾並在其中新建 package.json 文件。回想一下 package.json 的工做原理,其中完整的列出了該工程的依賴、項目名稱、做者等信息。咱們新工程中的 package.json 大體以下:express

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

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

npm install express -savejson

命令執行完成後,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.content %>
             </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 的動態渲染而且語法也更爲友好。

原文地址

相關文章
相關標籤/搜索