# 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`,可以看到 ![](https://i.imgur.com/9PNQGOt.png) 改為連接 `localhost:3000/about` ![](https://i.imgur.com/RvdovEP.png) 但不管連到哪個頁面,瀏覽器卻都不斷的在轉圈圈 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'}); }); ``` 執行,可以看到不論切到哪個連結,在伺服器端都會執行剛剛新增的中介軟體 ![](https://i.imgur.com/StmHVtE.png) ![](https://i.imgur.com/E4ju9EV.png) ![](https://i.imgur.com/LrygDYp.png) <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'}); }); ``` 執行,可以看到只有 `'/'` 路徑不會觸發新增的中介軟體 ![](https://i.imgur.com/pN2DVqG.png) ![](https://i.imgur.com/b3ZmPba.png) ![](https://i.imgur.com/NvHzQKV.png) 因為負責處理 `'/'` 路徑的中介軟體在觸發後,沒有執行 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` ![](https://i.imgur.com/tNLN8LR.png) 可開啟 package.json 確認已安裝 morgan ![](https://i.imgur.com/4CmAyq7.png) <br /> 開始使用,首先在 app.js 就像第三方套件一樣引用 morgan ```javascript const morgan = require('morgan'); ``` <br /> 觀看 morgan 文件提供的 API 為 `morgan(format,options)`,我們可以使用該 API 建立一個 morgan 記錄器的中介軟體函數,其中 `format` 參數有三種形式 ![](https://i.imgur.com/obWq9iV.png) 這裡會示範使用第一種方式「預定義的格式化字串」,根據文件提供的選項有多種,這裡採用 `'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'}); }); ``` 執行,在瀏覽器點擊各連結,再回到伺服器的終端顯示以下資訊 ![](https://i.imgur.com/HGP68BM.png) 以上每行訊息分別對應 dev 格式會印出的五種訊息 ![](https://i.imgur.com/Q0cHYmB.png) 顏色的部分可以看到前三個選項為 304 重定向的狀態代碼,表示已讀取過的圖片或網頁,由瀏覽器緩存(cache) 中讀取,顏色顯示為青色。 至於最後一個由於輸入一個不存在的 url 所以得到 404 找不到的狀態,顏色顯示為黃色。 <br /> 可以將中介軟體函數改成 `'tiny'` 選項 ```javascript app.use(morgan('tiny')); ``` 結果其實與 dev 差不多,只是順序有些不一樣,且沒有顏色提示 ![](https://i.imgur.com/pRqtbAe.png) <br /> 以上為第三方中介軟體的簡單示範,有了這些第三方的中介軟體讓我們不用每次都從頭編碼所有的功能。 <br /> ## static files 靜態檔案 前面的筆記提過希望能有一個獨立的 CSS 檔案,而不是寫在 head 的 style 標籤裡,這個問題可以透過 Express 內建的中介軟體來解決。 首先在根目錄下建立 style.css 檔案 ![](https://i.imgur.com/U2CLscM.png) <br /> **style.css** 內容 ```css= body { background: black; } ``` 接著開啟 head.ejs,因為我們要在 head 使用 link 標籤引入 CSS 檔案 ![](https://i.imgur.com/sjfZ0ra.png) <br /> 在 head 標籤中添加以下 ```htmlmixed <link rel="stylesheet" href="/style.css"> ``` 儲存後執行,開啟網頁但沒有變化,背景沒有變成黑色,開啟開發人員工具顯示 style.css 找不到 ![](https://i.imgur.com/icpCq9e.png) 在網址輸入 `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 圖片檔 ![](https://i.imgur.com/ZIHYmTS.png) <br /> 現在可以重新整理瀏覽器,發現網頁背景變黑了 ![](https://i.imgur.com/rSdPYbb.png) <br /> 連接 `localhost:3000/style.css` 及 `localhost:3000/picture.jpg` 可以看到檔案了 **style.css** ![](https://i.imgur.com/XPONjtM.png) **picture.jpg** ![](https://i.imgur.com/VlJCjTh.png) <br /> 這邊可以注意到 head.ejs 中,樣式檔的路徑我們是寫 `"/style.css"` 而不是 `"/public/style.css"`,因為我們已在 app.js 設定 public 向瀏覽器是公開的,它會自動的搜尋 public 資料夾 ![](https://i.imgur.com/J4BK2U8.png) 在瀏覽器中尋找 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 的官方文件了解更多。