[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); console .log('path: ' , req.path); 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); console .log('path: ' , req.path); 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); console .log('path: ' , req.path); 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(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 ); 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.css
及 localhost: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 { 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 ; } .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