要學框架,從不用框架先開始
如何用 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'
})
範例小結
- node js 提供的模組,可以拿到 url、回覆 response、可以設計出一個簡單的 http server
- 使用 node.js 內建工具就可以實做 server
初探 Express
node.js 一個 frame work 叫做 Express。
- 建立一個資料夾 => mkdir
- npm init => 全 enter => 生成出一個 package.json
- npm install express --save
- 建立一個 index.js
- 執行 node.js => 出現 log 表示跑起來了
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
小結
- express 幫我們進行各種 http method 的包裝:
app.get()
、app.post()
、app.delete()
。要對什麼樣的 method 做反應都可以這樣寫。 - 學習 express basic route。route (路由)表示我要將這個 request 給誰處理然後導向哪裡。
與之前 Apache + PHP 組合的差別在哪?
Apache + PHP
- 瀏覽器會發 request 到 Apache server,所以如果 Apache server 掛了 php 是好的也沒有用。
- Apache server 再將東西丟給 php 處理
- php 處理完後回傳 response 給 Apache server
- Apache server 再將接收到的結果傳回去給瀏覽器
特點:
- Apache server 預設:路徑長甚麼樣子,在資料夾底下就會有那個名字的 php 檔
express
express 無 php 處理器這種東西,他自己本身就是一個 server。
- 瀏覽器發 request 給 express server
- express server 傳 response 給瀏覽器
特點:
- 透過路由系統決定什麼樣的網址要回傳什麼樣的資料。而不用像 Apache + PHP 侷限在檔案系統,我的檔案長甚麼樣我的 url 就長甚麼樣。依據 url 不同回傳不同結果,也不用有不同 js 檔案,所以我不用有一個叫做 hello.js 的檔案
小結
- Apache + PHP 透過 Apache server 丟給 php 處理;express server 直接處理所有東西。
- Apache + PHP 透過檔案系統處理;express server 透過路由系統處理。
Express 基本架構與 MVC
MVC 是 model view controller 的縮寫。MVC 是概念的定義,中間如何互通不同模式會有不同實作。
以下為我們遵循的實作模式,也是 server-side 比較常見的實作
- request 進來後交給 controller 處理。
- controller 負責跟 model 拿資料。(model 負責管資料)
- model 將資料回傳給controller,controller 裡面就有從 model 拿回來的 data。
- controller 呼叫 view 將 data 塞入 view,所以 view 就會有一個完整的 response 出來。(view 類似模板)
- 原本的 template 加上 data 就是完整的 response,view 再將組合好的東西回傳給 controller
- 最後 controller 在將 response 回傳給 request
controller 就是 model 與 view 中間的協調者,model 只管 data,view 只管 template 與顯示相關的,controller 負責管其他東西。
- MVC 的分工非常明確,讓維護上變得更加方便
node.js 如何完成以上架構
一、express 裡面如何實作 view
guide => using template engines
- 選擇一個 template engines 使用,選擇 ejs
npm install ejs
- 設定
app.set('view engine', 'ejs')
- 它的預設目錄會是 views,新增一個資料夾叫 views,裡面放一個檔案叫 hello.ejs,裡面寫入
<h1>hello</h1>
- 調整 index.js,表示我要叫 express render views 底下 hello 這個檔案。副檔名不用填,它會自己判斷。
app.get('/hello', (req, res) => {
res.render('hello')
})
- 瀏覽器上輸入:
http://localhost:5001/hello
,就出現用好的模板:hello.ejs 裡面的內容。 - 只要改 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
小結
- 結合 view 的 template engine 去輸出東西,這邊也可以加 header footer 變得更複雜
- 使用 express 寫出靜態畫面
以 MVC 重構專案
- 建立一個 models 的資料夾,裡面放 todo.js
- 建立一個 controllers 的資料夾,裡面放 todo.js
- 最後去原本的 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 溝通。
npm install mysql
- 觀察使用方法:填入參數、connect => 就好了
- 建立一個資料庫:localhost、database:app、todos 的 table 裡面有 id 與 content
- 建立 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 都分開,程式碼變得更簡潔了。