什麼是 middleware(中間件)?
Express 是一個本身功能極簡的路由與中介軟體 Web 架構:本質上,Express 應用程式是一系列的中介軟體函數呼叫。摘自官網
上述的中介軟體就是 middleware。
白話版:express 這套框架處理從收到 request 一直到發出 response 中間就是經過一系列 middleware 處理然後產生 response。
舉例來說,之前學習 express 的時候,做的重構版 index.js 裡面 app.get('/todos', todoController.getAll)
,在 /todos 後就交給 todoController.getAll,我們可以將 todoController.getAll 看成就是一個 middleware。
在這個 middleware 裡面永遠都可以接收到 req, res 兩個參數,然後去輸出我想要的東西。
const todoController = {
getAll: (req, res) => {
// 將資料從 model 拿出來
todoModel.getAll((err, result) => {
if (err) return console.log(err)
res.render('todos', {
todos: result
})
})
},
get: (req, res) => {
const id = req.params.id
// 回傳的資料會是 array
const todo = todoModel.get(id, (err, result) => {
if (err) return console.log(err)
res.render('todo', {
todo: result[0]
})
})
}
}
甚麼是一系列呢 ?
app.use( )
表示整個應用程式都可以用這個 middleware。app.use((req, res, next) => {})
,next 表示將控制權發到下一個 middleware 去。
index.js 加入這一行
app.use((req, res) => {
console.log('Time : ', new Date())
res.end()
})
重新整理 http://localhost:5001/todos
,網頁一片白,但 console 出現 Time : 2021-08-06T08:55:17.380Z
,每重新整理 console 就會再 log 一次。
因為沒有 call next,所以本來的 response 就沒有了。call next 表示將控制權交給下一個 middleware。
app.use((req, res, next) => {
console.log('Time : ', new Date())
next()
})
這次重新整理網頁就有東西,console 一樣會印出現在時間
middleware 在 express 的功用
express 內建是沒有去解析 delete、post 的內容,預設是沒有解析 request body 的內容,這就要靠 middleware 去完成。
express 內建也沒有 session 管理機制,所以也要靠 middleware。
express 內建有 req.query 可以抓網址列的 query string。但如果是 post 就必須依靠 middleware。
- url:
http://localhost:5001/test?a=1&b=3
,console 呈現 { a: '1', b: '3' }
app.get('/test', (req, res) => {
console.log(req.query)
})
middleware 是有順序性的,
假設我們要做一個簡單的權限管理機制,只有在網址列上看到 admin,才會看到 todo 的東西。
最直覺寫法:兩個 controller 裡面都加入同一個程式碼
controllers 裡面的 todo.js
function checkPermission(req) {
if (req.query.admin === '1') {
return true
} return false
}
===
function checkPermission(req) {
return req.query.admin === '1'
}
在 method 裡面加入權限檢查 if(!checkPermission(req)) return res.end()
const todoModel = require('../models/todo')
function checkPermission(req) {
return req.query.admin === '1'
}
const todoController = {
getAll: (req, res) => {
if(!checkPermission(req)) return res.end()
todoModel.getAll((err, result) => {
if (err) return console.log(err)
res.render('todos', {
todos: result
})
})
},
get: (req, res) => {
if (!checkPermission(req)) return res.end()
const id = req.params.id
const todo = todoModel.get(id, (err, result) => {
if (err) return console.log(err)
res.render('todo', {
todo: result[0]
})
})
}
}
現在,http://localhost:5001/todos
網頁一片白,傳入 ?admin=1 => http://localhost:5001/todos?admin=1
,就看的到東西了。
但 function 一多,會變得很煩,所以不是個好做法
以 middleware 寫
用 .use((req, res, next) => {})
瀏覽器跑 http://localhost:5001/todos?admin=1
會顯示畫面、但 admin 改成 2 就會顯示 Error
app.use 是整個 app 都會被影響到
// index.js 加入這段
app.use((req, res, next) => {
if (req.query.admin === '1') {
next()
} else {
res.end('Error')
}
})
可以對個別的路由進行添加與處理
function checkPermission(req, res, next) {
if (req.query.admin === '1') {
next()
} else {
res.end('Error')
}
}
app.get('/todos', checkPermission, todoController.getAll) // 只有這個路由會被影響
app.get('/todos/:id', todoController.get)
瀏覽器 http://localhost:5001/todos?admin=1
會顯示東西、http://localhost:5001/todos
出現 Error、但只有單一 todo 沒有問題 ex:http://localhost:5001/todos/2
,因為單一 todo 沒有加上 checkPermission 的限制。
小結
- middleware 的使用:接收 request、response,next 是表示是否把控制權交到下一個 middleware 去
解析 Request 必備:body-parser
非常重要的 middleware:body-parser
因為 express 的內建只能拿到 url 的 query string,所以如果 post 拿不到東西,但藉由 body-parser 這個 middleware 我可以拿到 request body 裡面的資料,換句話說 post 過來的東西我拿的到了。
- 安裝指令:
npm install body-parser
- 使用方法:參考文件與下方範例
- 引入進來
const bodyParser = require('body-parser')
- 決定要 parse 哪一種 content-type,一般來說下面兩個都會用到所以一起 parse 進來。
- 加完 content-type 後可以用 req.body 拿資料
// parse application/x-www-form-urlencoded
// 一般在瀏覽器上 post 的資料會是這個 content-type
app.use(bodyParser.urlencoded({ extended: false }))
// parse application/json
// ajax 會是這個 content-type
app.use(bodyParser.json())
實作一個新增 todo 的功能
- index.js 新增一個
app.get('/', todoController.addTodo)
- controller 新增 addTodo。這裡只是 render 頁面,並不是真的處理 addTodo 動作
addTodo: (req, res) => { res.render('addTodo')}
- views 新增 addTodo.ejs
- 現在 git bash 執行 node index.js => 瀏覽器去到根目錄網頁會 render addTodo.ejs => 輸入東西提交後會顯示 Cannot POST /todos,因為我們還沒處理 route 所以 express 也不知道該怎麼辦
- 在 index.js 新增一個路由 =>
app.post('/todos', todoController.newTodo)
=> controllers 裡面建立一個 newTodo,用req.body.input的 name
將東西拿出來,拿完後先進行輸出看結果是否正確 => 正確的印出我輸入的東西
// addTodo.ejs
<h1>Add Todo</h1>
<form method = "POST" action="/todos">
Content: <input type="text" name="content"/>
<input type="submit"/>
</form>
// controller 裡面的 todo.js
newTodo: (req, res) => {
const content = req.body.content
res.end(content)
}
- 若不加 content-type => TypeError: Cannot read property 'content' of undefined,也就是 request.body 是 undefined => 因為沒有用這個 middleware,所以沒有去做 request.body 的解析,因此就沒有東西出現 undefined
- controller 裡的 newTodo 拿到東西後去 call todoModel.add( )
- 處理 todoModel.add( ) => 到 models 裡進行 todo.js 的處理
newTodo: (req, res) => {
const content = req.body.content
todoModel.add(content, (err) => {
if (err) return console.log(err)
res.redirect('/todos')
})
}
models 裡的 todo.js
add: (content, cb) => {
db.query(
'insert into todos(content) values(?)', [content],
(err, results) => {
if (err) return cb(err)
cb(null)
}
)
}
view 裡面 todos.ejs 加上 <a href="/">add todo</a>
小結
- MVC 後端小專案、路由明確
- 一定要有 body-parser 才能處理 post 過來的資料。
負責管理 Session 的 Session middleware
npm install express-session
- 一樣在 index.js 上 require 進來
const session = require('express-session')
- 建立 session 的 middleware
// 參數待查
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}))
用法很像 php,背後機制可以看 source code
$_SESSION['abc'] // php
req.session.abc // 這邊
- 製作 login,一律是 req.session
- addTodo 加上參數 isLogin
app.get('/login', (req, res) => {
res.render('login')
})
app.post('/login', (req, res) =>{
if (req.body.password === 'abc') {
req.session.isLogin = true
res.redirect('/')
} else {
res.redirect('/login')
}
})
addTodo: (req, res) => {
res.render('addTodo', {
isLogin: req.session.isLogin
})
}
- 調整 view 裡面的 addTodo.ejs
- 製作 loging.ejs
// addTodo.ejs 加上下列 code
<% if (isLogin) {%>
你已經登入
<% } else { %>
尚未登入成功
<% } %>
// login.ejs
<h1>Login</h1>
<form method="POST" action="/login">
password: <input type="password" name="password"/>
<input type="submit">
</form>
- 製作 logout 功能
- view 加上 logout 按鈕
// index.js
app.get('/logout', (req, res) => {
req.session.isLogin = false
res.redirect('/')
})
// addTodo.ejs
<h1>Add Todo</h1>
<% if (isLogin) {%>
你已經登入 <a href="/logout">logout</a>
<% } else { %>
尚未登入成功
<% } %>
小結
- 熟悉 session 用法
- 思考問題:如果我需要在每個頁面都顯示是否登入,每個 render 的地方都要加
isLogin: req.session.isLogin
,所以我可以怎麼辦呢?
顯示錯誤訊息神器:connect-flash
flash messenger,Messages are written to the flash and cleared after being displayed to the user,背後用了 session 機制。
實作過程可參考 source code
npm install connect-flash
- First, setup sessions
- 引入
const flash = require('connect-flash')
- 加上
app.use(flash())
- 實際使用給予 key、value 就可以設置訊息,就可以跨頁面讀訊息
req.flash('info', 'Flash is back!')
- login.ejs 補上
<h2><%= errorMessage %></h2>
因為會導回根目錄頁面,所以在這邊這麼做。使用方式很像 session
// index.js
app.get('/login', (req, res) => {
res.render('login', {
errorMessage: req.flash('errorMessage') // 拿出資料
})
})
app.post('/login', (req, res) =>{
if (req.body.password === 'abc') {
req.session.isLogin = true
res.redirect('/')
} else {
req.flash('errorMessage', 'Password Incorrect') // 寫入資料
res.redirect('/login')
}
})
如果這個錯誤訊息在很多地方都用的到,express 提供了一個捷徑,讓我們自定義自己的 middleware
給 view 用的 global 變數:res.locals
res.locals 裡面的東西可以在 view 裡面拿到。views 可以用任何來自 request.locals 的東西,就像全域變數
拿 session、flash 都是用 req
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}))
// 將它放在 app.use(session 後面
app.use((req, res, next) => {
res.locals.isLogin = req.session.isLogin || false
res.locals.errorMessage = req.flash('errorMessage')
next() // 少了這個,request 會卡在這邊
})
// index.js
app.get('/login', (req, res) => {
res.render('login')
})
// controllers- todo.js
addTodo: (req, res) => {
res.render('addTodo')
}
//
小結
- 運用 req.flash 實作 flash messenger
- 放在 res.locals 的東西,就可以在 view 裡面直接被存取到,就不用每個 render 的地方都把東西放進去。是否登入、錯誤訊息都很適合放在這邊。
目標
// 使用 express 框架,server 跑完要連到 db
// 後端資料庫的資料、使用 view 框架、使用 controllers
// get 路由 1. todos 所有資料 2. todos/id 拿到個別資料 3. bye 不用 mvc 架構呈現 bye bye
// 使用 middle 1. 權限管理 2. 新增 todo (用 body parser 處理 post 過來的資料) 3. login 機制 4. flash messenger