Node.js 已經成爲 Web 後臺開發圈一股不容忽視的力量,憑藉其良好的異步性能、豐富的 npm 庫以及 JavaScript 語言方面的優點,已經成爲了不少大公司開發其後臺架構的重要技術之一,而 Express 框架則是其中知名度最高、也是最受歡迎的後端開發框架。在這篇教程中,你將瞭解 Express 在 Node 內置 http 模塊的基礎上作了怎樣的封裝,並掌握路由和中間件這兩個關鍵概念,學習和使用模板引擎、靜態文件服務、錯誤處理和 JSON API,最終開發出一個簡單的我的簡歷網站。javascript
此教程屬於 Node.js 後端工程師學習路線的一部分,歡迎來 Star 一波,鼓勵咱們繼續創做出更好的教程,持續更新中~。
自從 Ryan Dahl 在 2009 年的 JSConf 正式推出 Node.js 平臺後,這門技術的使用率就如同坐了火箭通常迅速上升,成爲了最受喜好的後端開發平臺之一,而 Express 則是其中最爲耀眼的 Web 框架。在正式開始這篇教程以前,咱們將列舉一下這篇教程所須要的預備知識、所用技術和學習目標。css
本教程假定你已經知道了:html
讀完這篇教程後,你將學會前端
注意雖然數據庫是後端開發中很是重要的環節,但 Express 並不內置處理數據庫的模塊,須要額外的第三方庫提供支持。這篇教程將重點放在了 Express 相關的概念講解上,所以不會涉及數據庫的開發。在學完這篇教程後,你能夠瀏覽 Express 相關的進階教程。java
在講解 Express 以前,咱們先了解一下怎麼用 Node.js 內置的 http 模塊來實現一個服務器,從而可以更好地瞭解 Express 對底層的 Node 代碼作了哪些抽象和封裝。若是你尚未安裝 Node.js,能夠去官方網站下載並安裝。node
咱們將實現一個我的簡歷網站。建立一個文件夾 express_resume,並進入其中:linux
mkdir express_resume && cd express_resume
建立 server.js 文件,代碼以下:git
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,上面的代碼含義很清晰:github
hostname
和端口號 port
http.createServer
建立 HTTP 服務器,參數爲一個回調函數,接受一個請求對象 req
和響應對象 res
,並在回調函數中寫入響應內容(狀態碼 200,類型爲 HTML 文檔,內容爲 Hello World
)最後運行 server.js:web
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
是路由被觸發時的回調函數,在函數中能夠執行相應的業務邏輯Nodemon 是一款頗受歡迎的開發服務器,可以檢測工做區代碼的變化,並自動重啓。經過如下命令安裝 nodemon:
npm install nodemon --save-dev
這裏咱們將 nodemon 安裝爲開發依賴 devDependencies
,由於僅僅只有在開發時才須要用到。同時咱們在 package.json 中加入 start
命令,代碼以下:
{ "name": "express_resume", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "nodemon server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.17.1" }, "devDependencies": { "nodemon": "^2.0.2" } }
到了動手的時候了,咱們用 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
方法開啓服務器。
從這一步開始,咱們運行 npm start
命令便可開啓服務器,而且一樣能夠看到 Hello World 的內容,可是代碼卻簡單明瞭了很多。
提示在運行
npm start
以後,可讓服務器一直打開着,編輯代碼並保存後,Nodemon 就會自動重啓服務器,運行最新的代碼。
接下來咱們開始講解 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 訪問,也能看到想要的數據哦。
當咱們的網站規模愈來愈大時,把全部代碼都放在 server.js 中可不是一個好主意。「拆分邏輯」(或者說「模塊化」)是最多見的作法,而在 Express 中,咱們能夠經過子路由 Router
來實現。
const express = require('express'); const router = express.Router();
express.Router
能夠理解爲一個迷你版的 app
對象,可是它功能完備,一樣支持註冊中間件和路由:
// 註冊一箇中間件 router.use(someMiddleware); // 添加路由 router.get('/hello', helloHandler); router.post('/world', worldHandler);
最後,因爲 Express 中「萬物皆中間件」的思想,一個 Router
也做爲中間件加入到 app
中:
app.use('/say', router);
這樣 router
下的所有路由都會加到 /say
之下,即至關於:
app.get('/say/hello', helloHandler); app.post('/say/world', worldHandler);
到了動手環節,首先建立 routes 目錄,用於存放全部的子路由。建立 routes/index.js 文件,代碼以下:
const express = require('express'); const router = express.Router(); router.get('/', (req, res) => { res.render('index'); }); router.get('/contact', (req, res) => { res.render('contact'); }); module.exports = router;
建立 routes/api.js,代碼以下:
const express = require('express'); const router = express.Router(); router.get('/', (req, res) => { res.json({ name: '圖雀社區', website: 'https://tuture.co' }); }); router.post('/new', (req, res) => { res.status(201).json({ msg: '新的篇章,即將開始' }); }); module.exports = router;
最後咱們把 server.js 中老的路由定義所有刪掉,替換成剛剛實現的兩個 Router
,代碼以下:
const express = require('express'); const path = require('path'); const indexRouter = require('./routes/index'); const apiRouter = require('./routes/api'); const hostname = 'localhost'; const port = 3000; const app = express(); // ... app.use(express.static('public')); app.use('/', indexRouter); app.use('/api', apiRouter); app.use('*', (req, res) => { res.status(404).render('404', { url: req.originalUrl }); }); // ...
是否是瞬間清爽了不少呢!若是你服務器還開着,能夠測試一下以前的路由是否還能成功運行哦。這裏我貼一下用 Curl 測試 /api
路由的結果:
$ curl localhost:3000/api {"name":"圖雀社區","website":"https://tuture.co"} $ curl -X POST localhost:3000/api/new {"msg":"新的篇章,即將開始"}
至此,這篇教程也就結束了。所完成的網站的確很簡單,可是但願你能從中學到 Express 的兩大精髓:路由和中間件。掌握了這兩大概念以後,後續進階教程的學習也會輕鬆不少哦!
想要學習更多精彩的實戰技術教程?來 圖雀社區逛逛吧。