# Node.js(八) Middleware 中介軟體
###### tags: `Node.js`
<br />
## 什麼是 Middleware?
中介軟體基本為在獲取請求和發送回應之間,在伺服器上運行的代碼,例如前面學習到的 `app.get()`、`app.use()` 即是中介軟體,而兩者之間的差別在於 `app.get()` 只會針對使用 get 方法的請求觸發,且中介軟體在代碼中是自上而下的運行,因此中介軟體的載入順序很重要,這點會在學習完本節筆記了解到。
那麼中介軟體除了之前的使用方式之外,還能做什麼?
* 回應 404 頁面 (前面使用方式)
* 記錄每個向伺服器發出請求的詳細訊息
* 在一些受到保護的路徑頁面,用於身分驗證檢查
* 分析從請求發送過來的 JSON 數據
以上為常見的使用
<br />
先回顧之前的 app.js 代碼
```javascript=
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()` 放在最後的順序。
<br />
現在讓我們在代碼的頂端撰寫中介軟體,用來針對每個請求記錄訊息
```javascript=
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
```javascript=
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,所以後面負責響應頁面的中介軟體沒有發揮其功能,後面就讓我們接著解決這個問題。
<br />
## next()
想要在執行完一個中介軟體後還能繼續執行下去其實很簡單,只要在中介軟體的函數新增 next 參數,但 next 其實是另一個 callback function,調用它得以前進到下一個中介軟體。
```javascript=
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` 的訊息。
<br />
接下來讓我們再做一些實驗,驗證 next() 的作用,新增以下的 code 作為順序第二的中介軟體
```javascript=
app.use((req, res, next) => {
console.log('in the next middleware');
next();
});
```
全部的 code
```javascript=
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'});
});
```
執行,可以看到不論切到哪個連結,在伺服器端都會執行剛剛新增的中介軟體



<br />
現在我們再改變一下該中介軟體的順序,放在處理 `'/'` get請求的中介軟體後面
```javascript=
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(),所以後面的中介軟體就不會觸發了。
<br />
除了在 `app.use()` 編寫要執行的代碼外,也可以如下,可以說使用 `app.use()` 載入指定的中介軟體函數 `myLogger`
```javascript=
var myLogger = function (req, res, next) {
console.log('LOGGED');
next();
};
app.use(myLogger);
```
以上為自定義的中介軟體,既然為自定義,那麼也就有第三方提供的已編寫好的中介軟體可以使用。
<br />
## 第三方中介軟體
Node.js 有第三方的套件模組可以下載使用,而中介軟體也有第三方提供,安裝方式也是使用 npm install 指令,例如這裡示範安裝並使用稱為 morgan 的第三方中介軟體。
可以透過 npmjs.com 搜尋 morgan,它的作用就像我們前面做的自定義的中介軟體負責記錄請求的訊息。
開始安裝 morgan,在終端輸入 `npm install morgan`

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

<br />
開始使用,首先在 app.js 就像第三方套件一樣引用 morgan
```javascript
const morgan = require('morgan');
```
<br />
觀看 morgan 文件提供的 API 為 `morgan(format,options)`,我們可以使用該 API 建立一個 morgan 記錄器的中介軟體函數,其中 `format` 參數有三種形式

這裡會示範使用第一種方式「預定義的格式化字串」,根據文件提供的選項有多種,這裡採用 `'tiny'` 及 `'dev'` 示範,先來了解這兩種選項分別有什麼作用。
**tiny**
最小化的輸出。
**dev**
簡單明瞭的用顏色來表明 response status 的輸出。成功代碼為綠色,伺服器錯誤代碼為紅色,客戶端錯誤代碼為黃色,重定向代碼為青色,信息代碼為無色。
基本上都是格式化輸出的記錄。
<br />
接著來看看怎麼在 express 中使用第三方中介軟體函數
```javascript
// app.use(第三方中介軟體函數);
app.use(morgan('dev'));
```
<br />
然後刪除前面在 app.js 新增的兩個自定義中介軟體,因為這裡要用 morgan 代為效勞。
全部的 code
```javascript=
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 找不到的狀態,顏色顯示為黃色。
<br />
可以將中介軟體函數改成 `'tiny'` 選項
```javascript
app.use(morgan('tiny'));
```
結果其實與 dev 差不多,只是順序有些不一樣,且沒有顏色提示

<br />
以上為第三方中介軟體的簡單示範,有了這些第三方的中介軟體讓我們不用每次都從頭編碼所有的功能。
<br />
## static files 靜態檔案
前面的筆記提過希望能有一個獨立的 CSS 檔案,而不是寫在 head 的 style 標籤裡,這個問題可以透過 Express 內建的中介軟體來解決。
首先在根目錄下建立 style.css 檔案

<br />
**style.css** 內容
```css=
body {
background: black;
}
```
接著開啟 head.ejs,因為我們要在 head 使用 link 標籤引入 CSS 檔案

<br />
在 head 標籤中添加以下
```htmlmixed
<link rel="stylesheet" href="/style.css">
```
儲存後執行,開啟網頁但沒有變化,背景沒有變成黑色,開啟開發人員工具顯示 style.css 找不到

在網址輸入 `localhost:3000/style.css` 也顯示 404 的頁面,因為伺服器會自動的保護不受用戶訪問檔案,所以我們必須指定哪些檔案是允許訪問的,或者說公開哪些檔案使得我們也能使用。
關於克服這部分可以透過 Express 的內建中介軟體函數 `express.static`,負責在 Express 的應用程式中提供靜態的檔案,也就是 CSS 或是圖片等檔案。
使用方式如下,`express.static` 函數的參數為要提供靜態檔案的根目錄,像這裡我們以 public 作為該根目錄
```javascript
app.use(express.static('public'));
```
app.js 全部的 code
```javascript=
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'});
});
```
<br />
新增 public 資料夾,並將 style.css 移動到其中以及放入一張 picture.jpg 圖片檔

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

<br />
連接 `localhost:3000/style.css` 及 `localhost:3000/picture.jpg` 可以看到檔案了
**style.css**

**picture.jpg**

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

在瀏覽器中尋找 style.css 及 picture.jpg 的 url 同樣也不需要添加 public。
<br />
現在我們可以將 head.ejs 的 style 內容移到 style.css,如下
**head.ejs**
```htmlmixed=
<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>
```
<br />
**style.css**
```css=
@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 的官方文件了解更多。