Express 實戰(五):路由

BigNerdCoding發表於2017-09-16

Cover
Cover

作為 Express 中的最大特點之一,路由讓你可以將不同的請求對映到不同的中介軟體中。這一章我們將會深入學習這部分的內容,另外還包括如何在 Express 使用 HTTPS 以及部分 Express 4 中的新特性等等。當然,學習過程還是通過示例應用和程式碼的形式進行展現的。

什麼是路由?

假設,現在你嘗試通過 example.com/someone 訪問某人的推特或者微博主頁,你會發現該請求的 HTTP 內容大致如下:

GET /someone http/1.1

其中包含了 HTTP 請求使用的方法(GET),URI 資訊(/someone) 以及 HTTP 協議版本 (1.1)。Express 中的路由就是負責將其中的 HTTP 方法和 URI 這對組合對映到對應的中介軟體。簡單說就是, /about_me 的GET 請求會執行某個中介軟體而對於 /new_user 的 POST 請求則執行另一箇中介軟體。

下面我們通過一個簡單示例來看看到底路由時如何工作的。

路由的一個簡單示例

下面我們就對 example.com/someone 請求進行一個簡單的實現,程式碼如下:

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

app.get('/someone', function(request, response) {
    response.send(" Welcome to someone's homepage! ");
});

app.use(function(request, response) {
    response.status(404).send("Page not found!");
});

app.listen(3000);複製程式碼

上面程式碼中真正有價值的是第三行:當你通過 HTTP 的 GET 方法對 /someone 發起請求時,程式會執行該中介軟體中的程式碼,其他請求則會被忽略並跳轉到下一個中介軟體。

路由的特性

從工作原理來說:路由就是通過對 HTTP 方法和的 URI 的組合進行對映來實現對不同請求的分別處理。當然,除了上面那種最簡單的使用方式之外,Express 的路由還有更多實用的使用技巧和方法。

注意:在其它一些框架中(例如,Ruby on Rails )會有一個專門的檔案進行路由管理,但是 Express 中並沒有這樣的規定,你可以將路由按模組分開管理。

含參的通配路由

在上面的使用方式中使用的是全等判斷來進行路由匹配的。雖然對於 /someone 這類非常管用,但是對於形如 /users/1/users/2 這類 RESTful 路由就明顯不那麼友好了。因為如果將後者路由一一列出的話,不管是從工作量還是後期維護來說都是非常差開發體驗。針對這種情況,我們可以使用 Express 中含參的通配路由來解決。

該方法的工作原理就是,在路由中使用引數進行通配表示。而該引數所表示的具體數值會在變數 params 中獲取到,下面是簡單的程式碼示例:


app.get("/users/:userid", function(req, res) {
    // 將userId轉換為整型
    var userId = parseInt(req.params.userid, 10);
    // ...
});複製程式碼

這樣 RESTful 風格的動態路由就完全可以通過這種含參的通配路由進行處理。那麼無論是 /users/123 還是 /users/8 都會被對映到同一中介軟體。需要注意的是:雖然 /users/ 或者 /users/123/posts 不會被匹配,但是 /users/cake/users/horse_ebooks 確會被匹配到。所以,如果實現更精準的路由匹配的話就需要使用其他方式了。

使用正規表示式匹配路由

針對上面的問題,我們可以使用正則來對路由進行更精準的匹配。

注意:如果你對正規表示式部分的內容不熟悉的話,那麼我建議你去檢視該文件

假設現在我們只需要匹配 /users/123/users/456 這種通配引數為數字的動態路由的同時忽略其他路由格式,那麼可以將程式碼改為:

app.get(/^\/users\/(\d+)$/, function(req, res) {
    var userId = parseInt(req.params[0], 10);
    // ...
});複製程式碼

通過正規表示式程式碼對通配引數作為了嚴格限定:該引數必須是數字型別。

正規表示式可能閱讀起來並不是很友好,但是它卻可以實現對複雜路由匹配規則的準確定義。例如,你想匹配路由 /users/100-500 這類表示某個使用者範圍的列表頁面,那麼該正則如下:

app.get(/^\/users\/(\d+)-(\d+)$/, function(req, res) {

    var startId = parseInt(req.params[0], 10);

    var endId = parseInt(req.params[1], 10);
    // …
});複製程式碼

甚至你還可以作出更復雜的正則匹配路由定義,例如:匹配某個包含特定 UUID 的路由。UUID 是一長串 16 進位制的字串,大致如下:

xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx

如果,其中的 x 表示任何 16 進位制數字,而 y 只能是 8,9,A 或者 B 。那麼該路由的正則匹配就是:

var horribleRegexp = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i;
app.get(horribleRegexp, function(req, res) {
    var uuid = req.params[0];
    // ...
});複製程式碼

還有更多的使用示例就不一一列舉了。這裡只需記住一點:正規表示式可以讓你的路由匹配定義更上一層樓。

捕獲查詢引數

另一種常用的動態傳入 URL 引數的方法就是通過查詢字串(query string)。例如,當你使用谷歌搜尋 javascript-themed burrito 時,你可以會發現對應的 URL 可能是 www.google.com/search?q=ja…

如果 Google 是用 Express 進行實現的話(實際上不是),那麼可以這樣來獲取使用者傳入的資訊:

app.get("/search", function(req, res) {
    // req.query.q == "javasript-themed burrito"
    // ...
});複製程式碼

需要注意的是:查詢引數中存在其實存在著型別安全問題。例如:如果你訪問 ?arg=something 那麼 req.query.arg 就是一個字串型別,但是如果訪問的是 ?arg=something&arg=somethingelse 的話 req.query.arg 就變為了一個陣列型別。簡單來說:不要輕易的斷定查詢引數的型別。

使用 Router 劃分你的 app

伴隨著應用的擴張,程式中產生的路由也會越來越多。而對這些龐大的路由進行管理並不是一件輕鬆的事,不過好在 Express 4 新增了 Router (可以理解為路由器)特性。Router 的官方描述是:

Router 是一個獨立於中介軟體和路由的例項,你可以將 Router 看作是隻能執行執行中介軟體和路由的小心應用。而 Express 程式本身就內建了一個 Router 例項。

Router 的行為與中介軟體型別,它可以通過 .use() 來呼叫其他的 Router 例項。

換句話就是,可以使用 Router 將應用劃分為幾個小的模組。雖然對於一些小型應用來說這樣做可能是過度設計,但是一旦 app.js 中的路由擴張太快的話你就可以考慮使用 Router 進行模組拆分了。

注意:程式越大 Router 發揮的作用就越明顯。雖然這裡我不會編寫一個大型應用程式,但是你可以在你的腦海中對下面的示例功能進行無限擴張。

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

// 引入 API  Router
var apiRouter = require("./routes/api_router");

var app = express();
var staticPath = path.resolve(__dirname, "static");
app.use(express.static(staticPath));
// API  Router 檔案的呼叫
app.use("/api", apiRouter);
app.listen(3000);複製程式碼

如上所示,Router 的使用方式和之前的中介軟體非常類似。其實 Router 本質上就是中介軟體。在程式碼中我們將所有 /api 開頭的 URL 全部轉發到了 apiRouter 中了, 這意味著 /api/users/api/message 的處理都會在 apiRouter 中進行。

下面就是 api_router.js 檔案的一個簡單程式碼示例:

var express = require("express");
var ALLOWED_IPS = [
    "127.0.0.1",
    "123.456.7.89"
];
var api  = express.Router();
api.use(function(req, res, next) {
    var userIsAllowed = ALLOWED_IPS.indexOf(req.ip) !== -1;
    if(!userIsAllowed) {
        res.status(401).send("Not authorized!");
    } else {
        next();
    }
});
api.get("/users", function(req, res) { /* ... */ });
api.post("/users", function(req, res) { /* ... */ });
api.get("/messages", function(req, res) { /* ... */ });
api.post("/messages", function(req, res) { /* ... */ });
module.exports = api;複製程式碼

其實 Router 與 app.js 在功能上沒有任何區別,都是處理中介軟體和路由。最大的不同在於:Router 只能已模組形式存在並不能獨立執行。

參照示例,你可以在自己的應用中按模組劃分出更多的 Router 。

靜態檔案

除非應用是純 API 服務,否則總可能需要傳送靜態檔案。這些檔案可能是靜態圖片 CSS 樣式檔案或者是靜態 HTML 檔案。在前面文章的基礎之上,這部分將介紹更深入的部分內容。

靜態檔案中介軟體

因為前面章節對靜態檔案中介軟體實現進行過詳細介紹,所以這裡直接檢視程式碼:

var express = require("express");
var path = require("path");
var http = require("http");
var app = express():
// 設定你的靜態檔案路徑
var publicPath = pathresolve(dirname, "public");
// 從靜態資料夾中傳送靜態檔案
app.use(express.static(publicPath));
app.use(function(request, response) {
    response.writeHead(200, { "Content-Type": "text/plain"});
    reponse.end("Looks like you didn't find a static file.");
});
http.createServer(app).listen(3000);複製程式碼

修改靜態檔案的 URL

通常情況下,我們會把站點的靜態檔案 URL 路徑直接掛在域名後面,例如:jokes.edu 站點中的 jokes.txt 檔案 URL 樣式應該是 jokes.edu/jokes.txt

當然,你可可以按照自己的習慣給這些靜態檔案提供 URL 。例如,將一些無序但有趣的圖片存放在資料夾 offensive 中並將其中圖片的 URL 設定為 jokes.edu/offensive/p… 這種形式。那麼該樣式 URL 如何實現呢?

在 Express 中,我們可以使用指定字首的中介軟體來對靜態檔案 URL 進行自定義。所以上面問題的程式碼實現如下:

// ... 
var photoPath = path.resolve(__dirname, "offensive-photos-folder");
app.use("/offensive", express.static(photoPath));
// ...複製程式碼

這樣你所有靜態檔案的 URL 都可以實現自定義了,而不是粗暴的直接掛在域名後面了。其實除了靜態中介軟體和前面 Router 外,其它中介軟體同樣可以指定 URL 字首。

多個靜態資料夾的路由

實際上砸真實專案中可能戶存在多個靜態資料夾,例如:一個存放 CSS 等公用檔案的 public 資料夾,一個存放使用者上傳檔案的 user_uploads 資料夾。那麼對於這種情況又該如何處理呢?

首先 epxress.static 本身作為中介軟體是可以在程式碼中多次呼叫的:

// ...
var publiscPath = path.resolve(__dirname, "public");
var userUploadPath = path.resove(__dirname, "user_uploads");
app.use(express.static(publicPath));
app.use(express.static(userUploadsPath));
// ...複製程式碼

接下來,我們通過四個模擬場景看看上面程式碼是如何工作的:

  1. 使用者請求的資源兩個資料夾裡都沒有則上面兩個中介軟體都會被跳過執行。
  2. 使用者請求的資源只在 public 裡面則第一個中介軟體響應執行並返回。
  3. 使用者請求的資源只在 user_uploads 裡面則第一個中介軟體被跳過而第二個得道執行。
  4. 使用者請求的資源在兩個資料夾中都存在則第一個中介軟體響應執行並返回,第二個不會得到執行。

對於第四章情況,如果該資源是相同的還好說,但是一旦只是資源同名就存在明顯錯誤了。為此,我們依舊可以使用 URL 字首來應對:

// ...
app.use("/public", express.static(publicPath));
app.use("/uploads", express.static(userUploadsPath));
// ...複製程式碼

這樣對於同名檔案 image.jpg Express 會將其分別對映到 /public/image.jpg/uploads/image.jpg

路由到靜態檔案對映

在程式中有可能還存在對動態路由請求響應靜態檔案情形,例如,當使用者訪問 /users/123/profile_photo 路徑時程式需要傳送該使用者的圖片。靜態中介軟體本身時無法處理該需求,不過好在 Express 可以使用與靜態中介軟體類似的機制來處理這種情況。

假設當有人發起 /users/:userid/profile_photo 請求時,我們都需要響應對應 userid 使用者的圖片。另外,假設程式中存在一個名為 getProfilePhotoPath 的函式,該函式可以根據 userid 獲取圖片的儲存路徑。那麼該功能的實現程式碼如下:

app.get("/users/:userid/profile_photo", function(req, res) {
    res.sendFile(getProfilePhotoPath(req.params.userid));
});複製程式碼

僅僅只需指定路由然後通過 sendFile 函式,我們就可以完成該路由對應檔案的傳送任務。

在 Express 使用 HTTPS

HTTPS 是在 HTTP 基礎上新增了一個安全層,通常情況下該安全層被稱為 TLS 或者 SSL 。雖然兩個名字可以互換,但是 TSL 在技術上涵蓋了 SSL。

這裡並不會介紹 HTTPS 複雜的 RSA 加密數學原理(尤拉函式)。簡單來說 HTTPS 的加密過程就是:所有的客戶端都使用服務端公開的公鑰加密請求資訊,然後服務端使用私鑰對加密後內容進行解密。這樣就能在某種程度上防止資訊被竊聽。另外,加密的公鑰也被稱為證照。客戶端在拿到公鑰證照後會向 Google 這樣的證照頒發機構進行驗證。

注意:類似 Heroku 這樣的虛擬主機商已經提供了 HTPPS 服務,所以這部分內容只在你需要自己實現 HTTPS 時才派得上用場。

首先,我們通過 OpenSSL 生成自簽名的公鑰和私鑰。Windows 系統可以使用去官網獲取 OpenSSL 安裝檔案,Linux 可以使用保管理器進行安裝,而 macOS 系統已經預裝過了。通過 openssl version 驗證系統是否成功安裝了 OpenSSL, 確保安裝後輸入下面兩個命令:

openssl genrsa -out privatekey.pem 1024
openssl req -new -key privatekey.pem -out request.pem

第一個命令會生成名為 privatekey.pem 的私鑰。第二個命令會讓你輸入一些資訊,然後使用 privatekey.pem 生成簽名的證照請求檔案 request.pem 。然後你就可以去證照請求機構申請一個加密的公鑰證照。雖然大部分證照都是收費的,但是你還是可以去 letsencrypt 申請免費版本證照。

一旦獲取了 SSL 證照檔案,你就可以使用 Node 內建的 HTTPS 模組了,程式碼如下:

var express = require("express");
var https = require("https");
var fs = require("fs");
var app = express();
// ... 定義你的app ...
// 定義一個物件來儲存證照和私鑰
var httpsOptions = {
    key: fs.fs.readFileSync("path/to/private/key.pem");
    cert: fs.fs.readFileSync("path/to/certificate.pem");
}

https.createServer(httpsOptions, app).listen(3000);複製程式碼

除了配置私鑰和公鑰證照引數之外,其他部分與之前 HTTP 模組的使用時一致的。當然,如果你想同時支援 HTTP 和 HTTPS 協議的話也是可以的:

var express = require("express");
var http = require("http");
var https = require("https");
var fs = require("fs");
var app = express();
// ... 定義你的app ...
var httpsOptions = {
    key: fs.readFileSync("path/to/private/key.pem"),
    cret: fs.readFileSync("path/to/certificate.pem")
};
http.createServer(app).listen(80);
https.createServer(httpsOptions, app).listen(443);複製程式碼

需要注意的是 HTTP 和 HTTPS 協議 同時開啟時需要使用不同的埠號。

路由的應用示例

接下來,我們搭建一個簡單的 web 程式鞏固一下這章所學的路由內容。該應用的主要功能是通過美國的 ZIP 郵政編碼返回該地區的溫度。

示例使用的是美式郵政編碼,所以該示例只能在作者所在的美國正常使用。當然,你完全可以使用 H5 的 Geolocation API 對其進行改造。

示例主要包含兩個部分:

  1. 一個靜態頁,用於詢問使用者的 ZPI 編碼。使用者輸入編碼後會通過 AJAX 傳送非同步請求獲取天氣。
  2. 解析獲得 JSON 格式資料,並將結果對映 ZIP 編碼對應的動態路由上。

準備工作

在示例中需要使用的 Node 類庫有:Express、ForecastIO (用於獲取天氣資料)、Zippity-do-dah ( 將ZIP編碼轉為緯度/經度 )、EJS 模版引擎。

新建應用資料夾,並複製下面內容到 package.json 檔案中:

{
    "name": "temperature-by-zip",
    "private": true,
    "scripts": {
        "start": "node app.js"
    },
    "dependencies": {
        "ejs": "^2.3.1",
        "express": "^5.0.0",
        "forecastio": "^0.2.0",
        "zippity-do-dah": "0.0.x"
    }
}複製程式碼

使用 npm install 命令完成依賴項的安裝,並新建兩個資料夾:public 和 views。另外,示例程式還會用到 jQuery 和名為 Pure 的 CSS 框架。最後,你需要去 Forecast.io 官網 註冊開發賬號獲取 API 介面金鑰。

主入口程式碼

準備工作完成後,接下來就是編寫程式碼了。這裡我們從程式的主入口開始編寫 JavaScript 程式碼,新建 app.js 檔案並拷貝程式碼:

var path = require("path");
var express = require("express");
var zipdb = require("zippity-do-dah");
var ForecastIo = require("forecastio");
var app = express();
var weather = new ForecastIo("你的FORECAST.IO的API金鑰");

app.use(express.static(path.resolve(__dirname, "public")));
app.set("views", path.resolve(__dirname, "views"));
app.set("view engine", "ejs");

app.get("/", function(req, res) {
    res.render("index");
});

app.get(/^\/(\d{5})$/, function(req, res, next) {
    var zipcode = req.params[0];
    var location = zipdb.zipcode(zipcode);
    if (!location.zipcode) {
        next();
        return;
    }

    var latitude = location.latitude;
    var longitude = location.longitude;
    weather.forecast(latitude, longitude, function(err, data) {
        if (err) {
            next();
            return;
        }

        res.json({
            zipcode: zipcode,
            temperature: data.currently.temperature
        });
    });
});

app.use(function(req, res) {
    res.status(404).render("404");
});

app.listen(3000);複製程式碼

接下來就是使用 EJS 引擎編寫檢視檔案了。

兩個檢視

示例應用中會有兩個檢視:404 頁面和主頁。為了儘可能保持頁面風格的統一,這裡將會使用到模版技術。首先動手實現通用的 headerfooter 模版。

其中 views/header.ejs 檔案中的程式碼如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Temperature by ZIP code</title>
    <link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
    <link rel="stylesheet" href="/main.css">
</head>
<body>複製程式碼

緊接著就是 views/footer.ejs

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

完成上面通用模版之後,下面就可以實現 404 頁面 views/404.ejs 了:

<% include header %>
    <h1>404 error! File not found.</h1>
<% include footer %>複製程式碼

同樣的,主頁 views/index.ejs 程式碼如下:

<% include header %>
<h1>What's your ZIP code?</h1>
<form class="pure-form">
    <fieldset>
        <input type="number" name="zip" placeholder="12345" autofocus required>
        <input type="submit" class="pure-button pure-button-primary" value="Go">
    </fieldset>
</form>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="/main.js"></script>
<% include footer %>複製程式碼

上面頁面程式碼中使用了一些 Pure 框架裡的樣式來優化介面 UI 。

除此之外,我們還需要在 public/main.css 指定頁面佈局:

html {
    display: table;
    width: 100%;
    height: 100%;
}
body {
    display: table-cell;
    vertical-align: middle;
    text-align: center;
}複製程式碼

在該樣式檔案中,我們將頁面內容同時設定為了水平和垂直居中。

最後拷貝下面的程式碼,把缺失的 public/main.js 補充完整。

$(function() {
    var $h1 = $("h1");
    var $zip = $("input[name='zip']");
    $("form").on("submit", function(event) {
        // 禁止表單的預設提交
        event.preventDefault();
        var zipCode = $.trim($zip.val());
        $h1.text("Loading...");

        var request = $.ajax({
            url: "/" + zipCode,
            dataType: "json"
        });
        request.done(function(data) {
            var temperature = data.temperature;
            $h1.html("It is " + temperature + "° in " + zipCode + ".");
        });
        request.fail(function() {
            $h1.text("Error!");
        });
    });
});複製程式碼

執行示例程式

結束所有編碼任務後,下面我們通過 npm start 執行示例程式。當你訪問 http://localhost:3000 並輸入 ZIP 編碼後介面如下:

05_01
05_01

在這個簡單的示例中,我們使用了 Express 中的路由特性,另外還使用了 EJS 模版引擎來編寫檢視檔案。你可以在此基礎上繼續發揮想象力完善該示例。

總結

在本章中,我們學到了:

  • 從概念上知道了什麼是路由:進行 URL 和程式碼的對映的工具。
  • 簡單的路由以及常用對映處理。
  • 獲取路由中的引數。
  • Express 4 路由模組的新特性。
  • 將路由應用到中介軟體處理。
  • 如何在 Express 中使用 HTTPS。

原文地址

相關文章