一杯茶的時間,上手 Express 框架開發

Node.js 已經成爲 Web 後臺開發圈一股不容忽視的力量,憑藉其良好的異步性能、豐富的 npm 庫以及 JavaScript 語言方面的優點,已經成爲了不少大公司開發其後臺架構的重要技術之一,而 Express 框架則是其中知名度最高、也是最受歡迎的後端開發框架。在這篇教程中,你將瞭解 Express 在 Node 內置 http 模塊的基礎上作了怎樣的封裝,並掌握路由和中間件這兩個關鍵概念,學習和使用模板引擎、靜態文件服務、錯誤處理和 JSON API,最終開發出一個簡單的我的簡歷網站。javascript

此教程屬於 Node.js 後端工程師學習路線的一部分,歡迎來 Star 一波,鼓勵咱們繼續創做出更好的教程,持續更新中~。

舊時代:用內置 http 模塊實現一個服務器

自從 Ryan Dahl 在 2009 年的 JSConf 正式推出 Node.js 平臺後,這門技術的使用率就如同坐了火箭通常迅速上升,成爲了最受喜好的後端開發平臺之一,而 Express 則是其中最爲耀眼的 Web 框架。在正式開始這篇教程以前,咱們將列舉一下這篇教程所須要的預備知識、所用技術和學習目標。css

預備知識

本教程假定你已經知道了:html

  • JavaScript 語言基礎知識(包括一些經常使用的 ES6+ 語法)
  • Node.js 基礎知識,特別是異步編程(這篇教程主要用到的是回調函數)和 Node 模塊機制,還有 npm 的基本使用,能夠參考這篇教程進行學習
  • HTTP 協議基礎知識,瀏覽器和服務器之間是如何互動的

所用技術

  • Node.js:8.x 及以上
  • npm:6.x 及以上
  • Express.js:4.x

學習目標

讀完這篇教程後,你將學會前端

  • Express 框架的兩大核心概念:路由和中間件
  • 用 Nodemon 加速開發迭代
  • 使用模板引擎渲染頁面,並接入 Express 框架中
  • 使用 Express 的靜態文件服務
  • 編寫自定義的錯誤處理函數
  • 實現一個簡單的 JSON API 端口
  • 經過子路由拆分邏輯,實現模塊化
注意

雖然數據庫是後端開發中很是重要的環節,但 Express 並不內置處理數據庫的模塊,須要額外的第三方庫提供支持。這篇教程將重點放在了 Express 相關的概念講解上,所以不會涉及數據庫的開發。在學完這篇教程後,你能夠瀏覽 Express 相關的進階教程java

用內置 http 模塊建立服務器

在講解 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

  1. 導入 http 模塊
  2. 指定服務器的主機名 hostname 和端口號 port
  3. http.createServer 建立 HTTP 服務器,參數爲一個回調函數,接受一個請求對象 req 和響應對象 res,並在回調函數中寫入響應內容(狀態碼 200,類型爲 HTML 文檔,內容爲 Hello World
  4. 在指定的端口開啓服務器

最後運行 server.js:web

node server.js

用瀏覽器打開 localhost:3000,能夠看到 Hello World 的提示:

能夠發現,直接用內置的 http 模塊去開發服務器有如下明顯的弊端:

  • 須要寫不少底層代碼——例如手動指定 HTTP 狀態碼和頭部字段,最終返回內容。若是咱們須要開發更復雜的功能,涉及到多種狀態碼和頭部信息(例如用戶鑑權),這樣的手動管理模式很是不方便
  • 沒有專門的路由機制——路由是服務器最重要的功能之一,經過路由才能根據客戶端的不一樣請求 URL 及 HTTP 方法來返回相應內容。可是上面這段代碼只能在 http.createServer 的回調函數中經過判斷請求 req 的內容才能實現路由功能,搭建大型應用時力不從心

由此就引出了 Express 對內置 http 的兩大封裝和改進:

  • 更強大的請求(Request)和響應(Response)對象,添加了不少實用方法
  • 靈活方便的路由的定義與解析,可以很方便地進行代碼拆分

接下來,咱們將開始用 Express 來開發 Web 服務器!

新時代:用 Express 搭建服務器

在第一步中,咱們把服務器放在了一個 JS 文件中,也就是一個 Node 模塊。從如今開始,咱們將把這個項目變成一個 npm 項目。輸入如下命令建立 npm 項目:

npm init

接着你能夠一路回車下去(固然也能夠仔細填),就會發現 package.json 文件已經建立好了。而後添加 Express 項目依賴:

npm install express

在開始用 Express 改寫上面的服務器以前,咱們先介紹一下上面提到的兩大封裝與改進

更強大的 Request 和 Response 對象

首先是 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 請求方法,包括 getpostputdelete 等等
  • PATH 是客戶端訪問的 URI,例如 //about
  • HANDLER 是路由被觸發時的回調函數,在函數中能夠執行相應的業務邏輯

nodemon 加速開發

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 中間件的定義

在 Express 中,中間件就是一個函數:

function someMiddleware(req, res, next) {
  // 自定義邏輯
  next();
}

三個參數中,reqres 就是前面提到的 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 的異步事件循環,下降服務器的吞吐率。在實際生產中,推薦使用第三方優秀的日誌中間件,例如 morganwinston 等等。

運行服務器,而後用瀏覽器嘗試訪問各個路徑。這裏我訪問了首頁(localhost:3000)和 /hellolocalhost: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>

再次運行服務器,並訪問咱們的網站。首頁以下:

聯繫咱們頁面以下:

能夠看到樣式表和圖片都成功加載出來了!

處理 404 和服務器錯誤

人有悲歡離合,月有陰晴圓缺,服務器也有出錯的時候。HTTP 錯誤通常分爲兩大類:

  • 客戶端方面的錯誤(狀態碼 4xx),例如訪問了不存在的頁面(404)、權限不夠(403)等等
  • 服務器方面的錯誤(狀態碼 5xx),例如服務器內部出現錯誤(500)或網關錯誤(503)等等

若是你打開服務器,訪問一個不存在的路徑,例如 localhost:3000/what,就會出現這樣的頁面:

很顯然,這樣的用戶體驗是很糟糕的。

在這一節中,咱們將講解如何在 Express 框架中處理 404(頁面不存在)及 500(服務器內部錯誤)。在此以前,咱們要完善一下 Express 中間件的運做流程,以下圖所示:

這張示意圖和以前的圖有兩點重大區別:

  • 每一個路由定義本質上是一個中間件(更準確地說是一個中間件容器,可包含多箇中間件),當 URI 匹配成功時直接返回響應,匹配失敗時繼續執行下一個路由
  • 每一箇中間件(包括路由)不只能夠調用 next 函數向下傳遞、直接返回響應,還能夠拋出異常

從這張圖就能夠很清晰地看出怎麼實現 404 和服務器錯誤的處理了:

  • 對於 404,只需在全部路由以後再加一箇中間件,用來接收全部路由均匹配失敗的請求
  • 對於錯誤處理,前面全部中間件拋出異常時都會進入錯誤處理函數,可使用 Express 自帶的,也能夠自定義。

處理 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

在這篇教程的最後,咱們將實現一個很是簡單的 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 端口,看到返回了想要的數據:

或者你能夠用 PostmanCurl 訪問,也能看到想要的數據哦。

使用子路由拆分邏輯

當咱們的網站規模愈來愈大時,把全部代碼都放在 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 的兩大精髓:路由和中間件。掌握了這兩大概念以後,後續進階教程的學習也會輕鬆不少哦!

想要學習更多精彩的實戰技術教程?來 圖雀社區逛逛吧。

相關文章
相關標籤/搜索