Node.js 已經成爲 Web 後臺開發圈一股不容忽視的力量,憑藉其良好的異步性能、豐富的 npm 庫以及 JavaScript 語言方面的優點,已經成爲了不少大公司開發其後臺架構的重要技術之一,而 Express 框架則是其中知名度最高、也是最受歡迎的後端開發框架。在這篇教程中,你將瞭解 Express 在 Node 內置 http 模塊的基礎上作了怎樣的封裝,並掌握路由和中間件這兩個關鍵概念,學習和使用模板引擎、靜態文件服務、錯誤處理和 JSON API,最終開發出一個簡單的我的簡歷網站。javascript
此教程屬於Node.js 後端工程師學習路線的一部分,歡迎來 Star 一波,鼓勵咱們繼續創做出更好的教程,持續更新中~。css
自從 Ryan Dahl 在 2009 年的 JSConf 正式推出 Node.js 平臺後,這門技術的使用率就如同坐了火箭通常迅速上升,成爲了最受喜好的後端開發平臺之一,而 Express 則是其中最爲耀眼的 Web 框架。在正式開始這篇教程以前,咱們將列舉一下這篇教程所須要的預備知識、所用技術和學習目標。html
本教程假定你已經知道了:前端
讀完這篇教程後,你將學會java
提示node
雖然數據庫是後端開發中很是重要的環節,但 Express 並不內置處理數據庫的模塊,須要額外的第三方庫提供支持。這篇教程將重點放在了 Express 相關的概念講解上,所以不會涉及數據庫的開發。在學完這篇教程後,你能夠瀏覽 Express 相關的進階教程。linux
在講解 Express 以前,咱們先了解一下怎麼用 Node.js 內置的 http 模塊來實現一個服務器,從而可以更好地瞭解 Express 對底層的 Node 代碼作了哪些抽象和封裝。若是你尚未安裝 Node.js,能夠去官方網站下載並安裝。git
咱們將實現一個我的簡歷網站。建立一個文件夾 express_resume,並進入其中:github
mkdir express_resume && cd express_resume
複製代碼
建立 server.js 文件,代碼以下:web
const http = require('http');
const hostname = 'localhost';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end('Hello World\n');
});
server.listen(port, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
複製代碼
若是你熟悉 Node.js,上面的代碼含義很清晰:
hostname
和端口號 port
http.createServer
建立 HTTP 服務器,參數爲一個回調函數,接受一個請求對象 req
和響應對象 res
,並在回調函數中寫入響應內容(狀態碼 200,類型爲 HTML 文檔,內容爲 Hello World
)最後運行 server.js:
node server.js
複製代碼
用瀏覽器打開 localhost:3000,能夠看到 Hello World 的提示:
能夠發現,直接用內置的 http 模塊去開發服務器有如下明顯的弊端:
http.createServer
的回調函數中經過判斷請求 req
的內容才能實現路由功能,搭建大型應用時力不從心由此就引出了 Express 對內置 http 的兩大封裝和改進:
接下來,咱們將開始用 Express 來開發 Web 服務器!
在第一步中,咱們把服務器放在了一個 JS 文件中,也就是一個 Node 模塊。從如今開始,咱們將把這個項目變成一個 npm 項目。輸入如下命令建立 npm 項目:
npm init
複製代碼
接着你能夠一路回車下去(固然也能夠仔細填),就會發現 package.json 文件已經建立好了。而後添加 Express 項目依賴:
npm install express
複製代碼
在開始用 Express 改寫上面的服務器以前,咱們先介紹一下上面提到的兩大封裝與改進。
首先是 Request 請求對象,一般咱們習慣用 req
變量來表示。下面列舉一些 req
上比較重要的成員(若是不知道是什麼也不要緊哦):
req.body
:客戶端請求體的數據,多是表單或 JSON 數據req.params
:請求 URI 中的路徑參數req.query
:請求 URI 中的查詢參數req.cookies
:客戶端的 cookies而後是 Response 響應對象,一般用 res
變量來表示,能夠執行一系列響應操做,例如:
// 發送一串 HTML 代碼
res.send('HTML String');
// 發送一個文件
res.sendFile('file.zip');
// 渲染一個模板引擎併發送
res.render('index');
複製代碼
Response 對象上的操做很是豐富,而且還能夠鏈式調用:
// 設置狀態碼爲 404,並返回 Page Not Found 字符串
res.status(404).send('Page Not Found');
複製代碼
提示
在這裏咱們並無簡單地列舉 Request 和 Response 的所有 API ,由於圖雀社區的理念是——從實戰中學習和深化理解,拒絕枯燥的 API 記憶!
客戶端(包括 Web 前端、移動端等等)向服務器發起請求時包括兩個元素:路徑(URI)以及 HTTP 請求方法(包括 GET、POST 等等)。路徑和請求方法合起來通常被稱爲 API 端點(Endpoint)。而服務器根據客戶端訪問的端點選擇相應處理邏輯的機制就叫作路由。
在 Express 中,定義路由只需按下面這樣的形式:
app.METHOD(PATH, HANDLER)
複製代碼
其中:
app
就是一個 express
服務器對象METHOD
能夠是任何小寫的 HTTP 請求方法,包括 get
、post
、put
、delete
等等PATH
是客戶端訪問的 URI,例如 /
或 /about
HANDLER
是路由被觸發時的回調函數,在函數中能夠執行相應的業務邏輯到了動手的時候了,咱們用 Express 改寫上面的服務器,代碼以下:
const express = require('express');
const hostname = 'localhost';
const port = 3000;
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(port, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
複製代碼
在上面的代碼中,咱們首先用 express()
函數建立一個 Express 服務器對象,而後用上面提到的路由定義方法 app.get
定義了主頁 /
的路由,最後一樣調用 listen
方法開啓服務器。
像第一步那樣再次運行 server.js,一樣能夠看到 Hello World 的內容,可是代碼卻簡單明瞭了很多。
提示
若是以前的服務器還開着,記得按 Ctrl+C 關掉哦。每次修改代碼以後,都要手動關掉服務器而後再次運行才能生效。在後續的進階教程中,咱們會教你用更先進的工具邊修改代碼邊檢查效果!
接下來咱們開始講解 Express 第二個重要的概念:中間件(Middleware)。
中間件並非 Express 獨有的概念。相反,它是一種廣爲使用的軟件工程概念(甚至已經延伸到了其餘行業),是指將具體的業務邏輯和底層邏輯解耦的組件(可查看這個討論)。換句話說,中間件就是可以適用多個應用場景、可複用性良好的代碼。
Express 的簡化版中間件流程以下圖所示:
首先客戶端向服務器發起請求,而後服務器依次執行每一箇中間件,最後到達路由,選擇相應的邏輯來執行。
提示
這個是一個簡化版的流程描述,目的是便於你對中間件有個初步的認識,在後面的章節中咱們將進一步完善這一流程。
有兩點須要特別注意:
在 Express 中,中間件就是一個函數:
function someMiddleware(req, res, next) {
// 自定義邏輯
next();
}
複製代碼
三個參數中,req
和 res
就是前面提到的 Request 請求對象和 Response 響應對象;而 next
函數則用來觸發下一個中間件的執行。
注意
若是忘記在中間件中調用
next
函數,而且又不直接返回響應時,服務器會直接卡在這個中間件不會繼續執行下去哦!
在 Express 使用中間件有兩種方式:全局中間件和路由中間件。
經過 app.use
函數就能夠註冊中間件,而且此中間件會在用戶發起任何請求均可能會執行,例如:
app.use(someMiddleware);
複製代碼
經過在路由定義時註冊中間件,此中間件只會在用戶訪問該路由對應的 URI 時執行,例如:
app.get('/middleware', someMiddleware, (req, res) => {
res.send('Hello World');
});
複製代碼
那麼用戶只有在訪問 /middleware
時,定義的 someMiddleware
中間件纔會被觸發,訪問其餘路徑時不會觸發。
接下來咱們就開始實現第一個 Express 中間件。功能很簡單,就是在終端打印客戶端的訪問時間、 HTTP 請求方法和 URI,名爲 loggingMiddleware
。代碼以下:
// ...
const app = express();
function loggingMiddleware(req, res, next) {
const time = new Date();
console.log(`[${time.toLocaleString()}] ${req.method} ${req.url}`);
next();
}
app.use(loggingMiddleware);
app.get('/', (req, res) => {
res.send('Hello World');
});
// ...
複製代碼
注意
在中間件中寫
console.log
語句是比較糟糕的作法,由於console.log
(包括其餘同步的代碼)都會阻塞 Node.js 的異步事件循環,下降服務器的吞吐率。在實際生產中,推薦使用第三方優秀的日誌中間件,例如 morgan、winston 等等。
運行服務器,而後用瀏覽器嘗試訪問各個路徑。這裏我訪問了首頁(localhost:3000)和 /hello
(localhost:3000/hello,瀏覽器應該看到的是 404),能夠看到控制檯相應的輸出:
[11/28/2019, 3:54:05 PM] GET /
[11/28/2019, 3:54:11 PM] GET /hello
複製代碼
這裏爲了讓你初步理解中間件的概念,咱們只實現了一個功能很簡單的中間件。實際上,中間件不只能夠讀取 req
對象上的各個屬性,還能夠添加新的屬性或修改已有的屬性(後面的中間件和路由函數均可以獲取),可以很方便地實現一些複雜的業務邏輯(例如用戶鑑權)。
最後,咱們的網站要開始展現一些實際內容了。Express 對當今主流的模板引擎(例如 Pug、Handlebars、EJS 等等)提供了很好的支持,能夠作到兩行代碼接入。
提示
若是你不瞭解模板引擎,不用擔憂,這篇教程幾乎不須要用到它的高級功能,你只需理解成一個「升級版的 HTML 文檔」便可。
這篇教程將使用 Handlebars 做爲模板引擎。首先添加 npm 包:
npm install hbs
複製代碼
建立 views 文件夾,用於放置全部的模板。而後在其中建立首頁模板 index.hbs,代碼以下:
<h1>我的簡歷</h1> <p>我是一隻小小的圖雀,渴望學習技術,磨鍊實戰本領。</p> <a href="/contact">聯繫方式</a> 複製代碼
建立聯繫頁面模板 contact.hbs,代碼以下:
<h1>聯繫方式</h1> <p>QQ:1234567</p> <p>微信:一隻圖雀</p> <p>郵箱:mrc@tuture.co</p> 複製代碼
最後即是在 server.js 中配置和使用模板。配置模板的代碼很是簡單:
// 指定模板存放目錄
app.set('views', '/path/to/templates');
// 指定模板引擎爲 Handlebars
app.set('view engine', 'hbs');
複製代碼
在使用模板時,只需在路由函數中調用 res.render
方法便可:
// 渲染名稱爲 hello.hbs 的模板
res.render('hello');
複製代碼
修改後的 server.js 代碼以下:
// ...
const app = express();
app.set('views', 'views');
app.set('view engine', 'hbs');
// 定義和使用 loggingMiddleware 中間件 ...
app.get('/', (req, res) => {
res.render('index');
});
app.get('/contact', (req, res) => {
res.render('contact');
})
// ...
複製代碼
注意在上面的代碼中,咱們添加了 GET /contact
的路由定義。
最後,咱們再次運行服務器,訪問咱們的主頁,能夠看到:
點擊」聯繫方式「,跳轉到相應頁面:
一般網站須要提供靜態文件服務,例如圖片、CSS 文件、JS 文件等等,而 Express 已經自帶了靜態文件服務中間件 express.static
,使用起來很是方便。
例如,咱們添加靜態文件中間件以下,並指定靜態資源根目錄爲 public
:
// ...
app.use(express.static('public'));
app.get('/', (req, res) => {
res.render('index');
});
// ...
複製代碼
假設項目的 public 目錄裏面有這些靜態文件:
public
├── css
│ └── style.css
└── img
└── tuture-logo.png
複製代碼
就能夠分別經過如下路徑訪問:
http://localhost:3000/css/style.css
http://localhost:3000/img/tuture-logo.png
複製代碼
樣式文件 public/css/style.css 的代碼以下(直接複製粘貼便可):
body {
text-align: center;
}
h1 {
color: blue;
}
img {
border: 1px dashed grey;
}
a {
color: blueviolet;
}
複製代碼
圖片文件可經過這個 GitHub 上的連接下載,而後下載到 public/img 目錄中。固然,你也可使用本身的圖片,記得在模板中替換相應的連接就能夠了。
在首頁模板 views/index.hbs 中加入 CSS 樣式表和圖片:
<link rel="stylesheet" href="/css/style.css" /> <h1>我的簡歷</h1> <img src="/img/tuture-logo.png" alt="Logo" /> <p>我是一隻小小的圖雀,渴望學習技術,磨鍊實戰本領。</p> <a href="/contact">聯繫方式</a> 複製代碼
在聯繫模板 views/contact.hbs 中加入樣式表:
<link rel="stylesheet" href="/css/style.css" /> <h1>聯繫方式</h1> <p>QQ:1234567</p> <p>微信:一隻圖雀</p> <p>郵箱:mrc@tuture.co</p> 複製代碼
再次運行服務器,並訪問咱們的網站。首頁以下:
聯繫咱們頁面以下:
能夠看到樣式表和圖片都成功加載出來了!
人有悲歡離合,月有陰晴圓缺,服務器也有出錯的時候。HTTP 錯誤通常分爲兩大類:
若是你打開服務器,訪問一個不存在的路徑,例如 localhost:3000/what
,就會出現這樣的頁面:
很顯然,這樣的用戶體驗是很糟糕的。
在這一節中,咱們將講解如何在 Express 框架中處理 404(頁面不存在)及 500(服務器內部錯誤)。在此以前,咱們要完善一下 Express 中間件的運做流程,以下圖所示:
這張示意圖和以前的圖有兩點重大區別:
next
函數向下傳遞、直接返回響應,還能夠拋出異常從這張圖就能夠很清晰地看出怎麼實現 404 和服務器錯誤的處理了:
在 Express 中,能夠經過中間件的方式處理訪問不存在的路徑:
app.use('*', (req, res) => {
// ...
});
複製代碼
*
表示匹配任何路徑。將此中間件放在全部路由後面,便可捕獲全部訪問路徑均匹配失敗的請求。
Express 已經自帶了錯誤處理機制,咱們先來體驗一下。在 server.js 中添加下面這條」壞掉「的路由(模擬現實中出錯的情形):
app.get('/broken', (req, res) => {
throw new Error('Broken!');
});
複製代碼
而後開啓服務器,訪問 localhost:3000/broken
:
危險!
服務器直接返回了出錯的調用棧!很明顯,向用戶返回這樣的調用棧不只體驗糟糕,並且大大增長了被攻擊的風險。
實際上,Express 的默認錯誤處理機制能夠經過設置 NODE_ENV
來進行切換。咱們將其設置爲生產環境 production
,再開啓服務器。若是你在 Linux、macOS 或 Windows 下的 Git Bash 環境中,能夠運行如下命令:
NODE_ENV=production node server.js
複製代碼
若是你在 Windows 下的命令行,運行如下命令:
set NODE_ENV=production
node server.js
複製代碼
這時候訪問 localhost:3000/broken
就會直接返回 Internal Server Error(服務器內部錯誤),不會顯示任何錯誤信息:
體驗仍是很很差,更理想的狀況是可以返回一個友好的自定義頁面。這能夠經過 Express 的自定義錯誤處理函數來解決,錯誤處理函數的形式以下:
function (err, req, res, next) {
// 處理錯誤邏輯
}
複製代碼
和普通的中間件函數相比,多了第一個參數,也就是 err
異常對象。
經過上面的講解,實現自定義的 404 和錯誤處理邏輯也就很是簡單了。在 server.js 全部路由的後面添加以下代碼:
// 中間件和其餘路由 ...
app.use('*', (req, res) => {
res.status(404).render('404', { url: req.originalUrl });
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).render('500');
});
app.listen(port, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
複製代碼
提示
在編寫處理 404 的邏輯時,咱們用到了模板引擎中的變量插值功能。具體而言,在
res.render
方法中將須要傳給模板的數據做爲第二個參數(例如這裏的{ url: req.originalUrl }
傳入了用戶訪問的路徑),在模板中就能夠經過{{ url }}
獲取數據了。
404 和 500 的模板代碼分別以下:
<link rel="stylesheet" href="/css/style.css" /> <h1>找不到你要的頁面了!</h1> <p>你所訪問的路徑 {{ url }} 不存在</p> 複製代碼
<link rel="stylesheet" href="/css/style.css" /> <h1>服務器好像開小差了</h1> <p>過一下子再試試看吧!See your later~</p> 複製代碼
再次運行服務器,訪問一個不存在的路徑:
訪問 localhost:3000/broken
:
體驗很不錯!
在這篇教程的最後,咱們將實現一個很是簡單的 JSON API。若是你有過其餘後端 API 開發(特別是 Java)的經驗,那麼你必定會以爲用 Express 實現一個 JSON API 端口簡單得難以想象。在以前提到的 Response 對象中,Express 爲咱們封裝了一個 json
方法,直接就能夠將一個 JavaScript 對象做爲 JSON 數據返回,例如:
res.json({ name: '百萬年薪', price: 996 });
複製代碼
會返回 JSON 數據 { "name": "百萬年薪", "price": 996 }
,狀態碼默認爲 200。咱們還能夠指定狀態碼,例如:
res.status(502).json({ error: '公司關門了' });
複製代碼
會返回 JSON 數據 { "error": "公司關門了"}
,狀態碼爲 502。
到了動手環節,讓咱們在 server.js 中添加一個簡單的 JSON API 端口 /api
,返回關於圖雀社區的一些數據:
// ...
app.get('/api', (req, res) => {
res.json({ name: '圖雀社區', website: 'https://tuture.co' });
});
app.get('/broken', (req, res) => {
throw new Error('Broken!');
});
// ...
複製代碼
咱們能夠用瀏覽器訪問 localhost:3000/api 端口,看到返回了想要的數據:
或者你能夠用 Postman 或 Curl 訪問,也能看到想要的數據哦。
至此,這篇教程也就結束了。所完成的網站的確很簡單,可是但願你能從中學到 Express 的兩大精髓:路由和中間件。掌握了這兩大概念以後,後續進階教程的學習也會輕鬆不少哦!
想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。