筆記、[BE201] 後端中階:Express


Posted by s103071049 on 2021-08-06

要學框架,從不用框架先開始

如何用 node.js 寫 http 的 server。server 就只是一個程式,所以在 node.js 一樣有提供一些內建 library 讓我們去建立一個 server。

  • createServer 裡面要傳的是 function
  • 命名慣例會用前三個字
  • http server 最常見到是 80 port,只是做測試用 5001 比較不會和其他人撞到

執行代碼會發現 console 沒有跑出任何東西,但他還在跑。這表示 http server 已經跑起來了,他一直跑著才能接收 request 回傳 response。

const http = require('http')
const server = http.createServer(handler)

function handler(req, res) {
  console.log(req.url)
  res.write('hello!') // response 內容為 hello
  res.end() // 將 response 給回傳
}

server.listen(5001)
  • 在瀏覽器輸入 http://localhost:5001/ 表示我要連到自己電腦的 5001 port => 網頁顯示 hello => git bash 顯示 //favicon.ico,瀏覽器會預設問你有無 icon 若有會顯示出來。檢視原始碼會有一個 .ico 檔案,這就是 tab 上的 logo。
  • 隨便輸入 http://localhost:5001/123344,會發現網頁內容相同,但 git bash 變成 /123344/favicon.ico

可以根據 request.url 決定輸出不同的內容。

const http = require('http')
const server = http.createServer(handler)

function handler(req, res) {
  console.log(req.url)
  if (req.url === '/hello') {
    res.write('hello!')
  } else if (req.url === '/bye') {
    res.write('bye')
  } else {
    res.write('Invalid url')
  }
  res.end()
}

server.listen(5001)

現在 http://localhost:5001/ 畫面呈現 Invalid url;http://localhost:5001/hello 畫面呈現 hello!;http://localhost:5001/bye 畫面呈現 bye

所以可以根據 url 不同輸出不同內容。

但它也可以輸出 html 原始碼。

const http = require('http')
const server = http.createServer(handler)

function handler(req, res) {
  console.log(req.url)
  if (req.url === '/hello') {
    res.write('<h1>hello!</h1>')
  } else if (req.url === '/bye') {
    res.write('bye')
  } else {
    res.write('Invalid url')
  }
  res.end()
}

server.listen(5001)

更標準的寫法是 .writeHead(statusCode, 參數),這樣一來瀏覽器就會知道回傳內容的格式是甚麼,要如何解析這個網頁。可以在 dev-tool network 的 response headers 檢視。

    res.writeHead(200, {
      'Content-Type': 'text/html'
    })

也可以利用這樣的寫法進行重新導向的設計。 301 + Location 就可以把使用者導向其他地方,這是 http 的設計。

res.writeHead(301, {
      'Location': 'https://google.com'
    })

範例小結

  1. node js 提供的模組,可以拿到 url、回覆 response、可以設計出一個簡單的 http server
  2. 使用 node.js 內建工具就可以實做 server

初探 Express

node.js 一個 frame work 叫做 Express。

  1. 建立一個資料夾 => mkdir
  2. npm init => 全 enter => 生成出一個 package.json
  3. npm install express --save
  4. 建立一個 index.js
  5. 執行 node.js => 出現 log 表示跑起來了
  6. http://localhost:5001/bbb => 因為沒有對 /bbb 寫 handler 所以沒反應;http://localhost:5001/ => hello;http://localhost:5001/bye => bye bye
const express = require('express') // 引入 express 
const app = express() // express 引入進來的東西是一個 function,去執行 function,然後建立一個 app
const app2 = express() // 可以用相同手法建立多個 app
const port = 5001

// 處理不同的 request
// 第二個參數是 cb 處理進來的 request
app.get('/', (req, res) => {
  res.send('hello')
})

app.get('/hello', (req, res) => {
  res.send('hello baby')
})

app.get('/bye', (req, res) => {
  res.send('bye bye')
})

app.listen(port, () => {
  console.log(`example app listening on port ${port}`)
}) // 第一個參數為 port,第二個參數是 cb function

小結

  1. express 幫我們進行各種 http method 的包裝:app.get()app.post()app.delete()。要對什麼樣的 method 做反應都可以這樣寫。
  2. 學習 express basic route。route (路由)表示我要將這個 request 給誰處理然後導向哪裡。

與之前 Apache + PHP 組合的差別在哪?

Apache + PHP

  1. 瀏覽器會發 request 到 Apache server,所以如果 Apache server 掛了 php 是好的也沒有用。
  2. Apache server 再將東西丟給 php 處理
  3. php 處理完後回傳 response 給 Apache server
  4. Apache server 再將接收到的結果傳回去給瀏覽器

特點:

  • Apache server 預設:路徑長甚麼樣子,在資料夾底下就會有那個名字的 php 檔

express

express 無 php 處理器這種東西,他自己本身就是一個 server。

  1. 瀏覽器發 request 給 express server
  2. express server 傳 response 給瀏覽器

特點:

  • 透過路由系統決定什麼樣的網址要回傳什麼樣的資料。而不用像 Apache + PHP 侷限在檔案系統,我的檔案長甚麼樣我的 url 就長甚麼樣。依據 url 不同回傳不同結果,也不用有不同 js 檔案,所以我不用有一個叫做 hello.js 的檔案

小結

  1. Apache + PHP 透過 Apache server 丟給 php 處理;express server 直接處理所有東西。
  2. Apache + PHP 透過檔案系統處理;express server 透過路由系統處理。

Express 基本架構與 MVC

MVC 是 model view controller 的縮寫。MVC 是概念的定義,中間如何互通不同模式會有不同實作。

以下為我們遵循的實作模式,也是 server-side 比較常見的實作

  1. request 進來後交給 controller 處理。
  2. controller 負責跟 model 拿資料。(model 負責管資料)
  3. model 將資料回傳給controller,controller 裡面就有從 model 拿回來的 data。
  4. controller 呼叫 view 將 data 塞入 view,所以 view 就會有一個完整的 response 出來。(view 類似模板)
  5. 原本的 template 加上 data 就是完整的 response,view 再將組合好的東西回傳給 controller
  6. 最後 controller 在將 response 回傳給 request

controller 就是 model 與 view 中間的協調者,model 只管 data,view 只管 template 與顯示相關的,controller 負責管其他東西。

  • MVC 的分工非常明確,讓維護上變得更加方便

node.js 如何完成以上架構

一、express 裡面如何實作 view

guide => using template engines

  1. 選擇一個 template engines 使用,選擇 ejs npm install ejs
  2. 設定 app.set('view engine', 'ejs')
  3. 它的預設目錄會是 views,新增一個資料夾叫 views,裡面放一個檔案叫 hello.ejs,裡面寫入 <h1>hello</h1>
  4. 調整 index.js,表示我要叫 express render views 底下 hello 這個檔案。副檔名不用填,它會自己判斷。
app.get('/hello', (req, res) => {
  res.render('hello')
})
  1. 瀏覽器上輸入:http://localhost:5001/hello,就出現用好的模板:hello.ejs 裡面的內容。
  2. 只要改 template 不需要重開 node 的 process,只要存檔網頁就會顯示。

二、練習題:實作一個只能看 todo 的 app

res.render('檔案', 資料),es6 裡面 key value 相同 todos: todos 可以省略 todos

index.js

const express = require('express')
const app = express()
const port = 5001

app.set('view engine', 'ejs')

const todos = ['first todo', 'second todo', 'third todo']
app.get('/todos', (req, res) => {
  res.render('todos', {
    todos
  })
})

app.get('/hello', (req, res) => {
  res.render('hello')
})

app.get('/bye', (req, res) => {
  res.send('bye bye')
})

app.listen(port, () => {
  console.log(`example app listening on port ${port}`)
}) // 第一個參數為 port,第二個參數是 cb function

views 裡面建立 todos.ejs

  • ejs 語法裡用 <% js 程式碼放在這邊喔 %>,寫 js 程式碼
  • <%= 要輸出的東西>,前面加 = 表示東西要輸出。用等號進行輸出。

todo.ejs,依 index.js 的 data 順利輸出三個 todo

<h1>Todos</h1>
<ul>
<% for(let i=0; i<todos.length; i++) { %>
    <li><%= todos[i]%></li>
<% } %>
</ul>

回到 index.js,繼續進行修改

  • /todo/:id :id 表示它是一個不確定的參數,利用 const id = req.params.id 拿到網址列指定好的 id,params 後面接的必須是冒號後面的參數
app.get('/todos/:id', (req, res) => {
  const id = req.params.id
  const todo = todos[id]
  res.render('todo', {
    todo
  })
})

實作 todo 的 view
todo.ejs

<h1>Todo</h1>
<h2><%= todo %></h2>

瀏覽器輸入 http://localhost:5001/todos/1,會出現 Todo second todo

小結

  1. 結合 view 的 template engine 去輸出東西,這邊也可以加 header footer 變得更複雜
  2. 使用 express 寫出靜態畫面

以 MVC 重構專案

  1. 建立一個 models 的資料夾,裡面放 todo.js
  2. 建立一個 controllers 的資料夾,裡面放 todo.js
  3. 最後去原本的 index.js 修改檔案

models 資料夾底下的 todo.js

const todos = ['first todo', 'second todo', 'third todo']

const todoModel = {
  getAll: () => {
    return todos
  },
  get: id => {
    return todos[id]
  }
}
// 輸出幾個 method
module.exports = todoModel

controllers 資料夾底下的 todo.js

// 去 models 將資料拿出來
const todoModel = require('../models/todo')
const todoController = {
  getAll: (req, res) => {
    const todos = todoModel.getAll() // 將資料從 model 拿出來
    res.render('todos', {
      todos
    })
  },
  get: (req, res) => {
    const id = req.params.id
    const todo = todoModel.get(id)
    res.render('todo', {
      todo
    })
  }
}

module.exports = todoController

主資料夾底下的 index.js

const express = require('express')
const app = express()
const port = 5001

const todoController = require('./controllers/todo')

app.set('view engine', 'ejs')
app.get('/todos', todoController.getAll)

app.get('/todos/:id', todoController.get)

app.get('/bye', (req, res) => {
  res.send('bye bye')
})

app.listen(port, () => {
  console.log(`example app listening on port ${port}`)
}) // 第一個參數為 port,第二個參數是 cb function

小結

路由將 request 傳給 controller,controller 再跟 model 拿資料然後再 call view 的 render。

  • controller:跟 view 與 model 溝通
  • model :處理資料
  • view :負責顯示

Node.js 與 MySQL 的串接

沒有 express 也能與 MySQL 溝通,所以是 node.js 與 MySQL 溝通。

  1. npm install mysql
  2. 觀察使用方法:填入參數、connect => 就好了
  3. 建立一個資料庫:localhost、database:app、todos 的 table 裡面有 id 與 content
  4. 建立 db.js 試一下 git 文件內容

輸出結果為 RowDataPacket,看起來是一個自訂的資料格式
[
RowDataPacket { id: 1, content: 'apple' },
RowDataPacket { id: 2, content: 'ball' },
RowDataPacket { id: 3, content: 'car' },
RowDataPacket { id: 4, content: 'duck' },
RowDataPacket { id: 5, content: 'elephant' },
RowDataPacket { id: 6, content: 'garden' }
]

var mysql      = require('mysql');
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : '亂碼',
  password : '亂碼',
  database : 'app'
});

connection.connect();

connection.query('SELECT * FROM todos', function (error, results, fields) {
  if (error) throw error;
  console.log(results);
});

connection.end();
console.log(results[0].content); // 就會顯示第一個 todo 的東西 => apple

因為是 sql query 所以不可能一直等,因此要傳進 cb function,

重構

db.js 負責處理資料庫連線

var mysql      = require('mysql');
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : '亂碼',
  password : '亂碼',
  database : 'app'
});

module.exports = connection

index.js 將 conection require 進來。這邊先不做連線異常的錯誤處理。

const express = require('express')
const db = require('./db')
const app = express()
const port = 5001

const todoController = require('./controllers/todo')

app.set('view engine', 'ejs')
app.get('/todos', todoController.getAll)

app.get('/todos/:id', todoController.get)

app.get('/bye', (req, res) => {
  res.send('bye bye')
})

app.listen(port, () => {
  db.connect() // server 跑起來後連線到 db
  console.log(`example app listening on port ${port}`)
})

修改 model 變成非同步模式
todo.js 引入 connection,接著會變成用 cb 拿資料。
用字串拼接會有 sql injection 的風險,可以在文件中搜尋 prepare statement,參考它的實作。

const db = require('../db')

const todoModel = {
  getAll: (cb) => {
    db.query(
      'SELECT * FROM todos',
      (err, results) => {
        if (err) return cb(err)
        cb(null, results)
      }
    )
  },
  get: (id, cb) => {
    db.query(
      'SELECT * FROM todos where id = ?', [id],
      (err, results) => {
        if (err) return cb(err)
        cb(null, results)
      }
    )
  }
}
module.exports = todoModel

修改 controller
不特別做錯誤處理

// 去 models 將資料拿出來
const todoModel = require('../models/todo')
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]
      })
    })
  }
}

module.exports = todoController

修改 view
針對 todos.ejs

// 寫法一
<h1>Todos</h1>
<ul>
<% for(let i=0; i<todos.length; i++) { %>
    <li><%= todos[i].id %>: <%= todos[i].content%></li>
<% } %>
</ul>
// 寫法二
<h1>Todos</h1>
<ul>
<% for(let i=0; i<todos.length; i++) { %>
    <li><%= todos[i].id + ':' +todos[i].content %></li>
<% } %>
</ul>

把物件轉字串,就會出現 [object Object],所以 todo.ejs 這樣做

<h1>Todo</h1>
<h2><%= todo.content %></h2>

mvc 都分開,程式碼變得更簡潔了。


#Express







Related Posts

型別守衛(type guard)

型別守衛(type guard)

Day 106

Day 106

[FE302] React 基礎 ( hooks 版 ):React 基礎

[FE302] React 基礎 ( hooks 版 ):React 基礎


Comments