做爲 Express 中的最大特色之一,路由讓你能夠將不一樣的請求映射到不一樣的中間件中。這一章咱們將會深刻學習這部分的內容,另外還包括如何在 Express 使用 HTTPS 以及部分 Express 4 中的新特性等等。固然,學習過程仍是經過示例應用和代碼的形式進行展示的。javascript
假設,如今你嘗試經過 example.com/someone 訪問某人的推特或者微博主頁,你會發現該請求的 HTTP 內容大體以下:css
GET /someone http/1.1html
其中包含了 HTTP 請求使用的方法(GET),URI 信息(/someone) 以及 HTTP 協議版本 (1.1)。Express 中的路由就是負責將其中的 HTTP 方法和 URI 這對組合映射到對應的中間件。簡單說就是, /about_me 的GET 請求會執行某個中間件而對於 /new_user 的 POST 請求則執行另外一箇中間件。java
下面咱們經過一個簡單示例來看看到底路由時如何工做的。node
下面咱們就對 example.com/someone 請求進行一個簡單的實現,代碼以下:jquery
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 發起請求時,程序會執行該中間件中的代碼,其餘請求則會被忽略並跳轉到下一個中間件。git
從工做原理來講:路由就是經過對 HTTP 方法和的 URI 的組合進行映射來實現對不一樣請求的分別處理。固然,除了上面那種最簡單的使用方式以外,Express 的路由還有更多實用的使用技巧和方法。web
注意:在其它一些框架中(例如,Ruby on Rails )會有一個專門的文件進行路由管理,可是 Express 中並無這樣的規定,你能夠將路由按模塊分開管理。ajax
在上面的使用方式中使用的是全等判斷來進行路由匹配的。雖然對於 /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 就變爲了一個數組類型。簡單來講:不要輕易的判定查詢參數的類型。
伴隨着應用的擴張,程序中產生的路由也會愈來愈多。而對這些龐大的路由進行管理並非一件輕鬆的事,不過好在 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 路徑直接掛在域名後面,例如: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));
// ...複製代碼
接下來,咱們經過四個模擬場景看看上面代碼是如何工做的:
對於第四章狀況,若是該資源是相同的還好說,可是一旦只是資源同名就存在明顯錯誤了。爲此,咱們依舊可使用 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 函數,咱們就能夠完成該路由對應文件的發送任務。
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 對其進行改造。
示例主要包含兩個部分:
在示例中須要使用的 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 頁面和主頁。爲了儘量保持頁面風格的統一,這裏將會使用到模版技術。首先動手實現通用的 header 和 footer 模版。
其中 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 編碼後界面以下:
在這個簡單的示例中,咱們使用了 Express 中的路由特性,另外還使用了 EJS 模版引擎來編寫視圖文件。你能夠在此基礎上繼續發揮想象力完善該示例。
在本章中,咱們學到了:
原文地址