[Node.js] Middleware 中介軟體

什麼是 Middleware?

中介軟體基本為在獲取請求和發送回應之間,在伺服器上運行的代碼,例如前面學習到的 app.get()app.use() 即是中介軟體,而兩者之間的差別在於 app.get() 只會針對使用 get 方法的請求觸發,且中介軟體在代碼中是自上而下的運行,因此中介軟體的載入順序很重要,這點會在學習完本節筆記了解到。

那麼中介軟體除了之前的使用方式之外,還能做什麼?

  • 回應 404 頁面 (前面使用方式)
  • 記錄每個向伺服器發出請求的詳細訊息
  • 在一些受到保護的路徑頁面,用於身分驗證檢查
  • 分析從請求發送過來的 JSON 數據

以上為常見的使用

先回顧之前的 app.js 代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const express = require('express');
const app = express();

app.set('view engine', 'ejs');

app.listen(3000);

app.get('/', (req, res) => {
const blogs = [
{title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
];

res.render('index', { title: 'Home', blogs});
});

app.get('/about', (req, res) => {
res.render('about', { title: 'About'});
});

app.get('/blogs/create', (req, res) => {
res.render('create', { title: 'Create a new Blog'});
});

app.use((req, res) => {
res.status(404).render('404', { title: '404'});
});

app.use() 由於沒有限定對於特定網址的請求觸發,即對每個請求都會觸發回應 404 頁面,而目前所學一旦中介軟體觸發便不會執行其它的中介軟體,這也是為什麼將 app.use() 放在最後的順序。

現在讓我們在代碼的頂端撰寫中介軟體,用來針對每個請求記錄訊息

1
2
3
4
5
6
app.use((req, res) => {
console.log('new request made:');
console.log('host: ', req.hostname); // 域名 localhost
console.log('path: ', req.path); // 路徑,與 URL 屬性相似
console.log('method: ', req.method); // 請求使用方法
});

app.js 全部的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const express = require('express');
const app = express();

app.set('view engine', 'ejs');

app.listen(3000);

app.use((req, res) => {
console.log('new request made:');
console.log('host: ', req.hostname); // 域名 localhost
console.log('path: ', req.path); // 路徑,與 URL 屬性相似
console.log('method: ', req.method); // 請求使用方法
});

app.get('/', (req, res) => {
const blogs = [
{title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
];

res.render('index', { title: 'Home', blogs});
});

app.get('/about', (req, res) => {
res.render('about', { title: 'About'});
});

app.get('/blogs/create', (req, res) => {
res.render('create', { title: 'Create a new Blog'});
});

app.use((req, res) => {
res.status(404).render('404', { title: '404'});
});

現在執行,並打開瀏覽器連接 localhost:3000,可以看到

改為連接 localhost:3000/about

但不管連到哪個頁面,瀏覽器卻都不斷的在轉圈圈 loading 中,最後顯示連不上網站,這是因為前面說過的在運行完第一個匹配的中介軟體後,便不再執行其它的 code,所以後面負責響應頁面的中介軟體沒有發揮其功能,後面就讓我們接著解決這個問題。

next()

想要在執行完一個中介軟體後還能繼續執行下去其實很簡單,只要在中介軟體的函數新增 next 參數,但 next 其實是另一個 callback function,調用它得以前進到下一個中介軟體。

1
2
3
4
5
6
7
app.use((req, res, next) => {
console.log('new request made:');
console.log('host: ', req.hostname);
console.log('path: ', req.path);
console.log('method: ', req.method);
next(); // 前往下一個中介軟體
});

現在執行,再次連上 localhost:3000 或是其它的路徑可以發現能夠正常顯示頁面了,且伺服器端的終端上也依舊顯示 req 的訊息。

接下來讓我們再做一些實驗,驗證 next() 的作用,新增以下的 code 作為順序第二的中介軟體

1
2
3
4
app.use((req, res, next) => {
console.log('in the next middleware');
next();
});

全部的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const express = require('express');
const app = express();

app.set('view engine', 'ejs');

app.listen(3000);

app.use((req, res, next) => {
console.log('new request made:');
console.log('host: ', req.hostname);
console.log('path: ', req.path);
console.log('method: ', req.method);
next();
});

// 新增該中介軟體
app.use((req, res, next) => {
console.log('in the next middleware');
next();
});

app.get('/', (req, res) => {
const blogs = [
{title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
];

res.render('index', { title: 'Home', blogs});
});

app.get('/about', (req, res) => {
res.render('about', { title: 'About'});
});

app.get('/blogs/create', (req, res) => {
res.render('create', { title: 'Create a new Blog'});
});

app.use((req, res) => {
res.status(404).render('404', { title: '404'});
});

執行,可以看到不論切到哪個連結,在伺服器端都會執行剛剛新增的中介軟體



現在我們再改變一下該中介軟體的順序,放在處理 '/' get請求的中介軟體後面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const express = require('express');
const app = express();

app.set('view engine', 'ejs');

app.listen(3000);

app.use((req, res, next) => {
console.log('new request made:');
console.log('host: ', req.hostname); // 域名 localhost
console.log('path: ', req.path); // 路徑,與 URL 屬性相似
console.log('method: ', req.method); // 請求使用方法
next();
});

app.get('/', (req, res) => {
const blogs = [
{title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
];

res.render('index', { title: 'Home', blogs});
});

// 移動到這裡
app.use((req, res, next) => {
console.log('in the next middleware');
next();
});

app.get('/about', (req, res) => {
res.render('about', { title: 'About'});
});

app.get('/blogs/create', (req, res) => {
res.render('create', { title: 'Create a new Blog'});
});

app.use((req, res) => {
res.status(404).render('404', { title: '404'});
});

執行,可以看到只有 '/' 路徑不會觸發新增的中介軟體

因為負責處理 '/' 路徑的中介軟體在觸發後,沒有執行 next(),所以後面的中介軟體就不會觸發了。

除了在 app.use() 編寫要執行的代碼外,也可以如下,可以說使用 app.use() 載入指定的中介軟體函數 myLogger

1
2
3
4
5
6
var myLogger = function (req, res, next) {
console.log('LOGGED');
next();
};

app.use(myLogger);

以上為自定義的中介軟體,既然為自定義,那麼也就有第三方提供的已編寫好的中介軟體可以使用。

第三方中介軟體

Node.js 有第三方的套件模組可以下載使用,而中介軟體也有第三方提供,安裝方式也是使用 npm install 指令,例如這裡示範安裝並使用稱為 morgan 的第三方中介軟體。

可以透過 npmjs.com 搜尋 morgan,它的作用就像我們前面做的自定義的中介軟體負責記錄請求的訊息。

開始安裝 morgan,在終端輸入 npm install morgan

可開啟 package.json 確認已安裝 morgan


開始使用,首先在 app.js 就像第三方套件一樣引用 morgan

1
const morgan = require('morgan');

觀看 morgan 文件提供的 API 為 morgan(format,options),我們可以使用該 API 建立一個 morgan 記錄器的中介軟體函數,其中 format 參數有三種形式

這裡會示範使用第一種方式「預定義的格式化字串」,根據文件提供的選項有多種,這裡採用 'tiny''dev' 示範,先來了解這兩種選項分別有什麼作用。

tiny
最小化的輸出。

dev
簡單明瞭的用顏色來表明 response status 的輸出。成功代碼為綠色,伺服器錯誤代碼為紅色,客戶端錯誤代碼為黃色,重定向代碼為青色,信息代碼為無色。

基本上都是格式化輸出的記錄。

接著來看看怎麼在 express 中使用第三方中介軟體函數

1
2
// app.use(第三方中介軟體函數);
app.use(morgan('dev'));

然後刪除前面在 app.js 新增的兩個自定義中介軟體,因為這裡要用 morgan 代為效勞。

全部的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const express = require('express');
const morgan = require('morgan');
const app = express();

app.set('view engine', 'ejs');

app.listen(3000);

app.use(morgan('dev'));

app.get('/', (req, res) => {
const blogs = [
{title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
];

res.render('index', { title: 'Home', blogs});
});

app.get('/about', (req, res) => {
res.render('about', { title: 'About'});
});

app.get('/blogs/create', (req, res) => {
res.render('create', { title: 'Create a new Blog'});
});

app.use((req, res) => {
res.status(404).render('404', { title: '404'});
});

執行,在瀏覽器點擊各連結,再回到伺服器的終端顯示以下資訊

以上每行訊息分別對應 dev 格式會印出的五種訊息

顏色的部分可以看到前三個選項為 304 重定向的狀態代碼,表示已讀取過的圖片或網頁,由瀏覽器緩存(cache) 中讀取,顏色顯示為青色。

至於最後一個由於輸入一個不存在的 url 所以得到 404 找不到的狀態,顏色顯示為黃色。

可以將中介軟體函數改成 'tiny' 選項

1
app.use(morgan('tiny'));

結果其實與 dev 差不多,只是順序有些不一樣,且沒有顏色提示


以上為第三方中介軟體的簡單示範,有了這些第三方的中介軟體讓我們不用每次都從頭編碼所有的功能。

static files 靜態檔案

前面的筆記提過希望能有一個獨立的 CSS 檔案,而不是寫在 head 的 style 標籤裡,這個問題可以透過 Express 內建的中介軟體來解決。

首先在根目錄下建立 style.css 檔案


style.css 內容

1
2
3
body {
background: black;
}

接著開啟 head.ejs,因為我們要在 head 使用 link 標籤引入 CSS 檔案


在 head 標籤中添加以下

1
<link rel="stylesheet" href="/style.css">

儲存後執行,開啟網頁但沒有變化,背景沒有變成黑色,開啟開發人員工具顯示 style.css 找不到

在網址輸入 localhost:3000/style.css 也顯示 404 的頁面,因為伺服器會自動的保護不受用戶訪問檔案,所以我們必須指定哪些檔案是允許訪問的,或者說公開哪些檔案使得我們也能使用。

關於克服這部分可以透過 Express 的內建中介軟體函數 express.static,負責在 Express 的應用程式中提供靜態的檔案,也就是 CSS 或是圖片等檔案。

使用方式如下,express.static 函數的參數為要提供靜態檔案的根目錄,像這裡我們以 public 作為該根目錄

1
app.use(express.static('public'));

app.js 全部的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const express = require('express');
const morgan = require('morgan');
const app = express();

app.set('view engine', 'ejs');

app.listen(3000);

// middleware & static files
app.use(express.static('public'));
app.use(morgan('tiny'));

app.get('/', (req, res) => {
const blogs = [
{title: 'Nice to meet you', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'The weather is nice today', snippet: 'Lorem ipsum dolor sit amet consectetur.'},
{title: 'I am so happy', snippet: 'Lorem ipsum dolor sit amet consectetur.'}
];

res.render('index', { title: 'Home', blogs});
});

app.get('/about', (req, res) => {
res.render('about', { title: 'About'});
});

app.get('/blogs/create', (req, res) => {
res.render('create', { title: 'Create a new Blog'});
});

app.use((req, res) => {
res.status(404).render('404', { title: '404'});
});

新增 public 資料夾,並將 style.css 移動到其中以及放入一張 picture.jpg 圖片檔


現在可以重新整理瀏覽器,發現網頁背景變黑了


連接 localhost:3000/style.csslocalhost:3000/picture.jpg 可以看到檔案了

style.css

picture.jpg


這邊可以注意到 head.ejs 中,樣式檔的路徑我們是寫 "/style.css" 而不是 "/public/style.css",因為我們已在 app.js 設定 public 向瀏覽器是公開的,它會自動的搜尋 public 資料夾

在瀏覽器中尋找 style.css 及 picture.jpg 的 url 同樣也不需要添加 public。

現在我們可以將 head.ejs 的 style 內容移到 style.css,如下

head.ejs

1
2
3
4
5
6
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Joe | <%= title %></title>
<link rel="stylesheet" href="/style.css">
</head>

style.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif&display=swap');
body{
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
font-family: 'Noto Serif', serif;
max-width: 1200px;
}
p, h1, h2, h3, a, ul{
margin: 0;
padding: 0;
text-decoration: none;
color: #222;
}
/* nav & footer styles */
nav{
display: flex;
justify-content: space-between;
margin-bottom: 60px;
padding-bottom: 10px;
border-bottom: 1px solid #ddd;
text-transform: uppercase;
}
nav ul{
display: flex;
justify-content: space-between;
align-items: flex-end;
}
nav li{
list-style-type: none;
margin-left: 20px;
}
nav h1{
font-size: 3em;
}
nav p, nav a{
color: #777;
font-weight: 300;
}
footer{
color: #777;
text-align: center;
margin: 80px auto 20px;
}
h2{
margin-bottom: 40px;
}
h3{
text-transform: capitalize;
margin-bottom: 8px;
}
.content{
margin-left: 20px;
}
/* index styles */
/* details styles */
/* create styles */
.create-blog form{
max-width: 400px;
margin: 0 auto;
}
.create-blog input,
.create-blog textarea{
display: block;
width: 100%;
margin: 10px 0;
padding: 8px;
}
.create-blog label{
display: block;
margin-top: 24px;
}
textarea{
height: 120px;
}
.create-blog button{
margin-top: 20px;
background: crimson;
color: white;
padding: 6px;
border: 0;
font-size: 1.2em;
cursor: pointer;
}

我們已經將 CSS 完全的存放在單獨檔案了。

關於 Express 使用中介軟體的方式有更多的細節,需要時可以參考 Express 的官方文件了解更多。


參考資料
The Net Ninja | Node.js Crash Course Tutorial #8 - Middleware