[BE201] 後端中階:middleware (上)


Posted by s103071049 on 2021-08-06

什麼是 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 的限制。

小結

  1. middleware 的使用:接收 request、response,next 是表示是否把控制權交到下一個 middleware 去

解析 Request 必備:body-parser

非常重要的 middleware:body-parser

因為 express 的內建只能拿到 url 的 query string,所以如果 post 拿不到東西,但藉由 body-parser 這個 middleware 我可以拿到 request body 裡面的資料,換句話說 post 過來的東西我拿的到了。

  1. 安裝指令:npm install body-parser
  2. 使用方法:參考文件與下方範例
  • 引入進來 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 的功能

  1. index.js 新增一個 app.get('/', todoController.addTodo)
  2. controller 新增 addTodo。這裡只是 render 頁面,並不是真的處理 addTodo 動作 addTodo: (req, res) => { res.render('addTodo')}
  3. views 新增 addTodo.ejs
  4. 現在 git bash 執行 node index.js => 瀏覽器去到根目錄網頁會 render addTodo.ejs => 輸入東西提交後會顯示 Cannot POST /todos,因為我們還沒處理 route 所以 express 也不知道該怎麼辦
  5. 在 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) 
  }
  1. 若不加 content-type => TypeError: Cannot read property 'content' of undefined,也就是 request.body 是 undefined => 因為沒有用這個 middleware,所以沒有去做 request.body 的解析,因此就沒有東西出現 undefined
  2. controller 裡的 newTodo 拿到東西後去 call todoModel.add( )
  3. 處理 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>

小結

  1. MVC 後端小專案、路由明確
  2. 一定要有 body-parser 才能處理 post 過來的資料。

負責管理 Session 的 Session middleware

  1. npm install express-session
  2. 一樣在 index.js 上 require 進來 const session = require('express-session')
  3. 建立 session 的 middleware
// 參數待查
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}))

用法很像 php,背後機制可以看 source code

$_SESSION['abc'] // php 
req.session.abc // 這邊
  1. 製作 login,一律是 req.session
  2. 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
    })
  }
  1. 調整 view 裡面的 addTodo.ejs
  2. 製作 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>
  1. 製作 logout 功能
  2. 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 { %>
  尚未登入成功
<% } %>

小結

  1. 熟悉 session 用法
  2. 思考問題:如果我需要在每個頁面都顯示是否登入,每個 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

  1. npm install connect-flash
  2. First, setup sessions
  3. 引入 const flash = require('connect-flash')
  4. 加上 app.use(flash())
  5. 實際使用給予 key、value 就可以設置訊息,就可以跨頁面讀訊息 req.flash('info', 'Flash is back!')
  6. 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')
}
//

小結

  1. 運用 req.flash 實作 flash messenger
  2. 放在 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

#Middleware







Related Posts

版本控制 - GitHub實作

版本控制 - GitHub實作

React-[路由篇]-SPAs與React Router (上)

React-[路由篇]-SPAs與React Router (上)

[Oracle Debug] SQL Tuning For Dealing with Hard Parse

[Oracle Debug] SQL Tuning For Dealing with Hard Parse


Comments