# Node.js及MongoDB、MYSQL筆記 ### 前言 MongoDB是noSQL也就是非關聯性資料庫,不會像MySQL一樣每一個分頁的表格都互相關聯。 ### 特點 - 格式不固定 只需要JSON即可 - 大量資料與即時性資料可使用 - ## Node.js ### 起手 新建一個node專案,則使用npm init。 當輸入完所需資料後就會幫我們自動新建一個package.json #### 輸入的東西 - package name 專案名稱 - version 版本 - descript 專案描述 - entry point 程式進入點(第一個要讀的入口檔案) - test commend 測試專案要如何測試 - git repository 遠端git倉庫 - keyword 讓其他人在npm中搜尋此專案的關鍵字 - author 作者 - license 使用規範 輸入完成後package.json大概會如下 ``` { "name": "learn-project", // 專案名稱 "version": "0.0.1", // 版本 "description": "just learn", // 描述 "scripts": {}, // 專案指令 "main": "index.js", // 進入點 "author": "me", // 作者 "license": "MIT", // 授權 "private": true, // 是否為隱私 "dependencies": {} // 安裝的套件 } ``` #### 如何啟動npm專案 我們知道要使用node執行一個JS檔案就是輸入 node 檔名.js。 不過我們在使用npm的專案時都是使用 npm run serve / yarn run serve 並沒有使用node 去執行啊? 其實是有使用node的,只不過我們將node的指令封裝載package.json了。 只需要在package.json中的script封裝指令,即可使用npm執行我們所想要執行的動作。 ``` "scripts": { "serve": "node index.js" }, ``` 依照上方的程式碼封裝後,我們只需要輸入npm run serve那就代表我們輸入了node index.js的指令。 至於npm其他的指令這邊就不做多述。 ### 使用node建立http server 在打API時其實是瀏覽器在跟伺服器溝通,如果要寫API自然會需要一個伺服器讓使用可以與我們溝通。 而node本身其實就有自帶建立http server的功能。 以下會一步一步的來建立https server ``` import http from 'http'; const PORT = 3000; /** 建立server */ const server = http.createServer((req, res) => { /** 第一步驟定是先設定header 回傳一定需要有格式等資訊 */ res.writeHead(200, {'Content-Type':'text/html'}); /** 第二步驟 要回傳什麼 */ res.write('you want send content'); /** 第三步驟 回傳的結束 */ res.end(); }); /** * 第四步驟 監聽port * @desc 當port號被請求時執行server所設定的程序 */ server.listen(PORT); ``` #### port號是什麼? port可以說是伺服器對外的接口。 也可以想像他是一把鑰匙,只有持有鑰匙的人才可以進到指定房間。 舉個例子,你想打API請求自然就得去https or http的port處理。 你到了其他的port號是沒有用的。 當然這個port號是由處理的人來定義的。 如果我今天想把http port號定在9453,那我API連9453也是可以請求得到。 常用port號: - https port號 443 - http port號 80 - 21:FTP服務(檔案傳輸) - 23:SSH服務(遠端連接) - 3306:MYSQL服務 - 27017:MongoDB - 3000:前端測試port號 ### 關於API所需要知道的內容 #### client端及server端 我們知道網頁程式有分前端跟後端,那通常怎麼分呢? 其實會以處理API這塊下去做區分。 - client端 又稱前端 client端就是指發送請求以及接收對應請求回傳的那端 - server端 稱為後端 也就是接收請求的伺服器。 server端會根據請求調用資料庫並回傳response給client端 #### http的請求方式 API請求方式總共分為四種,分別是GET、POST、PUT/PATCH、DELETE - GET 與POST並列最常見的請求方式。傳遞資料的方式使用Domain帶參數。 通常用於沒有資安問題以及少量的傳輸 - POST 傳遞方式使用request body,通常資料的傳輸都靠它。 可傳遞大量的資料。 - PUT/PATCH PUT 為修改全部的資料 PATCH 為修改部分資料 - DELETE 顧名思義就是刪除的意思 雖然分成四種,但其實使用POST方式一路到底也可以喔,因為DB資料處理還是要回傳response都是API端在處理的。 所以資料要怎麼處理自然都是看寫出來的程式是如何操作的。過往的方式基本上都是POST一路到底。 在這邊有個專業的術語是CRUD。 C = created = POST / R = read = GET / U = update = PUT/PATCH / D = DELETE **備註: 其實我們只要前往網址,那都算是一種請求。** ## express 在理解完理論基礎以後,我們要來探索實務的部分。 追求開發速度及方便情況下,出現了許多框架與套件。 常見的前端框架有Vue、React等等。 而前端有框架,後端自然也有。 在Node.js中最常見的框架也就是express。 ### 特點 express不只完整的支持API功能,還有許多靈活性。 例如router、server啟動的功能都有支援。官方也有相關的說明。 ### 建立server 請直接看程式碼,接下來會在程式碼中加入註解來說明。 額外的會再進行補充。 ``` /** 首先引入並建立express實例 */ const express = require('express'); const app = express(); /** 設定port號 */ const PORT = 3000; /** 與HTTP建立server一樣,需要去監聽port號。第二個參數為server啟動時會觸發的callback */ app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`) }) ``` ### 建立API server有了,下一步自然就是API了,接下來開始建立我們的API ``` const express = require('express'); const app = express(); const PORT = 3000; /** * 依照請求建立API * @desc 第一個參數為路徑,第二個為要執行的function,可以理解成到指定路徑後要執行的動作 * 這邊的request是client端發送的請求,reponse則是我們server端回傳給client端的response */ app.get('/', (request, response)=>{ /** 設定status code */ response.status(200); /** 設定回傳內容 */ response.send('this is my first API'); }); /** 注意這邊listen一定要放在最後面 可以理解成設定完後依照設定啟動 */ app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` 如此以上就是簡單架設了一個server以及API。 當然,API的架設不可能單單只有這樣,後續會再一步一步的將API所需的知識拼湊起來。 ### 利用express讀取靜態檔案 在實作之前我們得先理解什麼是靜態檔案? 靜態檔案就是指我們不會去更動到的檔案都叫靜態,不管是圖片也好、JS等等的程式碼也好。 我們不會去更改到裡面的內容,統稱為靜態檔案。 舉個例子,圖片。我們只會引入圖片做使用,但我們不會去修改內容。 #### 放置的位置(keyword) 檔案總會需要個資料夾存放,而通常靜態檔案的資料夾名稱會有以下幾種: - assets - static - public #### 實作 理論理解後,我們就可以開始來實作了! ##### 第一步 建立檔案 首先我們要讀取檔案,理所當然會須要檔案。 請在package.json同層中建立一個名為public的資料夾,並在這個資料夾中建立一個images的資料夾。 將圖片放在裡面。 ##### 第二步 設定express.static路徑 ``` const express = require('express'); const app = express(); const PORT = 3000; /** * 掛載絕對路徑下的public * @desc 因為靜態檔案不只是圖片,所以這邊是抓public */ app.use(express.static(__dirname + '/public')); app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` ##### 第三步 確認有設定成功 輸入localhost:3000/images/{你設定的圖片}去查看是否有該圖片 有就代表設定成功。 當然,不只圖片,連HTML檔案等也可以查看喔! PS: 掛載後若要取得圖片,則不需要使用/public/images.png 去取得,因為當設定後自動會將public改為根目錄 ##### 補充: __dirname 絕對路徑 設定路徑時我們需要知道dirname(絕對路徑)是什麼。 絕對路徑就是以根目錄為起點一直到檔案or資料夾在裝置上的位置。 也就是會一路從最根目錄的路徑一路一直到指定的檔案/資料夾的路徑 example: /Users/georgehuang/Desktop/work/node-learn/public/images/pic_betarea.png 至於相對路徑就是只以該檔案/資料夾的那一層為起點,一直到檔案/資料夾的位置 PS: 路徑也可以往回退的 - ./ 當前目錄 - ../ 前一層目錄 - ~ 在mac/linux為家目錄 - @ 在前端框架中代表src ##### 補充: 請求的callback可以有多個 ## express 設計Router 到現在我們已經基本上了解express的基礎功能了, 接下來就是要往進階的方向去探索。例如模組化等等的功能。 畢竟前端都有元件模組化了,後端這個要處理更龐大的資料不可能將處理都塞在同一個檔案中對吧? ### router 路由/路徑的定義 設計之前我們先來理解何謂router。 router是路由又稱為路徑,就是在戳API時的那些/user or /api/test等等的東西。 拿上面我們建立的API來說,我們的路由就是 **'/'**,根據不同的需要我們建立不同的路由來處理對應的功能及需求。 RESTful的API就是透過這些路由以及http協議設計出來的 其定義為:判斷應用程式如何回應用戶端對特定端點的要求,而這個特定端點是一個URL或路徑與特定的http要求方法。 ### express.router() express的router其實就是將前面我們所設計的API模組化。 那我們就開始實作吧! 1. 第一步 建立routes資料夾 這個資料夾就是存放各個模組化router的地方。 在這邊我們在與index.js同層的地方建立一個routes的資料夾,並且在裡面新增一個檔案example.js的檔案 2. 撰寫router ``` const express = require('express'); /** 建立router實例 */ const router = express.Router(); /** * 一樣可以根據請求方式做處理 * 參數的req及res我們知道。就是reqeust及response * next則是middeware的功能,繼續前往下一個function */ router.get('/', (req, res, next) => { const person = [ { name: 'Amy', arg: '33', }, { name: 'George', arg: '25', }, ]; /** 將資料使用JSON格式回傳 */ res.json(person); console.log(next); }); /** 將router使用module的方式export出去 */ module.exports = router; ``` 3. 在入口點引用router ``` const express = require('express'); const app = express(); const PORT = 3000; const exampleRouter = require('./routes/example'); app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); /** * 設定該router路由 * @desc 因為剛才我們在router設定的是'/'根目錄 * 所以當要請求時就會是example/ * 依此類推,只要在router設定的路由都需要依此作延伸 * 例如在router有個路由設定是'/set' * 那我請求的路由就會長'url/example/set' */ app.use('/example',exampleRouter); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` ### statusCode代表的意義 - 200 請求成功 - 201 資源成功被建立 物件建立成功/修改 - 204 server沒做任何回應 不需要回應的功能,例如刪除時可能會是這個 - 301 永久移到新位置 - 302 暫時移到新位置 - 400 錯誤請求 代表request資料錯誤 - 401 未認證 - 403 沒有權限 - 404 找不到資源 資料庫沒有該資料 - 500 伺服器有誤 伺服器有報錯(後端出錯) - 503 伺服器暫無回應 伺服器重啟or部分功能維修中 ### 錯誤訊息的設定 其實錯誤的statusCode也是API設定的。 通常是為了避免出錯後前端有不可預期的顯示。 並且在回傳時也一樣會回傳錯誤訊息。 通常是API再出錯後,會執行error回傳 ``` const express = require('express'); const app = express(); const PORT = 3000; app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); /** 這邊只是做個範例 通常會在判斷後決定要傳送什麼 */ app.get('/error-example', (req, res)=> { res.status(500); const messages = {messages: 'server is sometion wrong'}; res.json(messages); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` ### MVC MVVM架構說明 #### MVC MVC架構中: - controll是負責發送request以及接收response - views 依照controll需求顯示網頁 - model 依照controll需求與DB進行溝通 正常的運行邏輯會是: ``` client端 -> controll跟Model要資料 -> controll拿著資料渲染views ``` #### MVVM MVVM架構中: - view 負責畫面及UI(包括事件的接收與綁定 例如click後 - viewModel 畫面邏輯、事件處理、資料暫存 - model 資料來源 互相資料流 正常運行邏輯: view綁定資料至viewModel -> 資料改變viewModel會去跟model處理資料 -> 處理完成後會回應到view端 or view綁定事件 -> 事件觸發後交由viewModel接手處理 -> viewModel與model交流處理資料 -> 回到viewModel來渲染view ## express middleware ### 什麼是middleware middleware被稱作是中介程式or中介軟體,主要處理request到response之間的事情,例如token的解析、紀錄log等等 當然不一定要在這之間,可以在這之前也可以在之後。 例如token就可能會設計在request之前,先進行身份驗證。 而紀錄log則是在request之後response之前。 ### 實作 在實作之前,我們先提一句,middleware就是一個函式,也是通過use去處理。 它的概念有點難以形容,從程式碼來瞭解就會比較能理解上述的說明。 ``` const express = require('express'); const app = express(); const PORT = 3000; /** 建立middleware 函式 */ const middleware = (req, res, next) => { /** * @desc 這邊紀錄路由、請求方式及時間 * 時間可以使用ISO or UTC統一後,後面要處理也比較方便不會有時區問題 */ console.log(`log:[${req.method}]${req.url}--- ${new Date().toISOString()}`); /** * @desc next是讓請求可以繼續往下執行,如果不加就會在這邊停下 * 是middleware的方法。同時其他的路由處理器也可以使用next() */ next(); }; app.use(express.static(__dirname + '/public')); /** 使用middleware */ app.use(middleware); app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); app.get('/test', (request, response)=>{ response.status(200); response.send('this test API'); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` ### 身份驗證 一般來說身份驗證都是透過JWTTOKEN等等的安全性較高的方式下去判斷。 不過我們現在只是以簡易的方式展現一下身份驗證大概如何處理,所以使用的是query。 query也就是domain後帶的參數。example:url?name=george ``` const express = require('express'); const app = express(); const PORT = 3000; /** 建立middleware 函式 */ const middleware = (req, res, next) => { /** * @desc 這邊紀錄路由、請求方式及時間 * 時間可以使用ISO or UTC統一後,後面要處理也比較方便不會有時區問題 */ console.log(`log:[${req.method}]${req.url}--- ${new Date().toISOString()}`); /** * @desc next是讓請求可以繼續往下執行,如果不加就會在這邊停下 * 是middleware的方法。同時其他的路由處理器也可以使用next() */ next(); }; /** 建立middleware 函式 */ const validationMiddleware = (req, res, next) => { const { query } = req; if(query.name !== 'george'){ const messages = { messages:'你沒有權限登入' }; /** 設定請求失敗的code以及訊息 */ res.status(401); res.json(messages); return; } /** 如果符合則直接往下執行 */ next(); }; app.use(express.static(__dirname + '/public')); /** 使用middleware */ app.use(middleware); /** 使用權限判斷middleware */ app.use(validationMiddleware); app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` **補充:如果是middleware一般都會命名middleware讓開發者知道這個是用在middleware上** ## express cookies && session處理 在說明以前,得先樹立個觀念,cookies及session並不是要二擇一。 兩個是相補的。 session是處理server端儲存。 cookies則是處理client端儲存。 ### cookies 在實作之前我們先理解cookies是什麼。 cookies通常會由server端發送給client端,然後儲存在瀏覽器。 在取得cookies後,每一次的請求client端都會帶上cookies 算是一種儲存資料的方式。 #### 為什麼會存在cookies? 因為http在發送請求時是不會有狀態這個東西,所以進而才產生。 #### 特點 優: - 只針對特定網域起作用 - 有期限,時間到了自然消失 - 請求時會一併帶入 缺: - 資料儲存在client端,懂使用的可以任一修改 - 資料儲存太多會影響傳輸,因為每次請求都會帶上 **備註: clint端在跨域請求時不會主動帶上cookies 但可以手動設定帶上** ### session session是屬於server端的儲存方式,並且會與cookies搭配。 clent端首次請求時,session會建立獨立的ID並儲存在server端。 同時也會透過response將這個獨立id儲存在前端的cookies中。 那下一次只要clinet端請求時,server端就會根據這個獨立的id來判斷請求的使用者是誰,給予對應的動作。 舉個例子,購物車。 如果是全端處理,那我們可以透過sessionID來判斷使用者是誰,並且顯示他所放入購物車的內容。 **需要注意!這邊的session以及前端的sessionStore不是指同一種東西。** #### 特點 - 存放server端,安全性較高 - 主要用途記錄使用者的的行為與資訊(sessionId是1對1) 例如登入資訊,雖然現在都使用JWTtoken比較多 - session不會消失,只有過期與否 - session通常會包一個cookies出去 #### 運作的流程 1. client端首次API請求 2. 在server端把使用者資訊存入session store,並生成session ID作為索引 3. response時會一併將session ID傳送給使用者並存放在瀏覽器(client端)的cookie 4. 下一次client端API請求時會自動將cookies給帶上 5. server端可以取得此次request中cookies的資料,然後與server端的session進行判斷及處理 #### 儲存的位置 產生的sessionId會儲存在以下位置 - 記憶體 - 資料庫 #### 實作 1. 首先得安裝express-session 2. 引入並使用 ``` const express = require('express'); const app = express(); const session = require('express-session'); // 引入session const PORT = 3000; /** * 設定session * @desc express-session是middleware,所以必須把它寫在路由之前。 * 把express-session寫在路由之前,所有的request都會生成一個session並可以透過req.session這個變數來取得session內容, * 以及req.sessionID來取得session ID。可以在後端用console log來觀察這些變數 */ app.use(session({ secret: 'node', resave: true, // 設定每次的request都會覆蓋舊server儲存的session存檔。 saveUninitialized: true, cookie:{ maxAge: 10 * 60 * 1000 // 單位是毫秒。1000毫秒 = 1秒,而10分鐘等於 60秒 * 10 } })); app.get('/', (req, res)=>{ /** * 設定session並且儲存在session store中 * @desc session後的key可以自行命名 */ req.session.test = '123'; res.status(200); /** * 在後端session store保存的同時也會透過response將session所產生的資料傳給前端 * 前端會存在cookies中 */ res.send('this homePage API' + req.session.test); }); /** * 倘若client端有cookies資料,那其他API一樣可以取到 * 注意這邊的是"取得"request而不是像上方的"賦予" * 所以這邊是判斷請求的資料中的有沒有這個key及value */ app.get('/test', (req, res) => { if(req.session.test){ res.status(200); res.send(req.session.test); return; } res.status(401); res.send('not found'); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` **這邊注意,我們所設定的session.test在cookies並不會看到,我們看見的cookies key會是設定的name或是預設的connect.sid。** #### express-session的參數設定 - **secret**:server端產生id的簽章 用於產生唯一值地方 - **name**: 存在client端的cookies key名稱 預設 connect.sid session是儲存在後端沒錯,但前端是儲存在cookies,兩者是相互運用。 - **resave**: 每次requst是否覆蓋舊的session - **saveUninitialized**: 保存初始化的session。可以理解為強制將未初始化的session存回 session store,未初始化的意思是它是新的而且未被修改。 這個如果設定是true,會把「還沒修改過的」session就存進session store。以登入的例子來說,就是使用者還沒登入,我還沒把使用者資訊寫入session,就先存放了session在session store。設定為false可以避免存放太多空的session進入session store。另外,如果設為false,session在還沒被修改前也不會被存入cookie。 - **store**: 儲存session的方式 預設是node記憶體 - **Cookies**: - **path**: cookies存在client端的位置 - **httpOnly**: 設定前端不能用JS訪問 - **secure**: 是否使用https - **maxAge**: 設定cookies存活時間 單位為毫秒 ### 實戰運用 登入機制 這邊會做一個以session搭配cookies做的登入機制,不過因為尚未使用到DB,所以資料會先坐在function中。 1. 第一步 建立登入API ``` app.post('/login', (req, res) => { const fackDB = [ { account: 'george123456', password: 'qwqw1212', user: 'george' }, { account: 'amy123456', password: 'qwqw1212', user: 'amy' }, ]; const onCheckLogin = (act, pad) => fackDB.find((user) => user.account === act && user.password === pad); const { account, password } = req.body; const foundUser = onCheckLogin(account, password); /** 若有使用者資訊代表帳密正確 登入成功 回傳成功訊息 */ if(foundUser){ req.session.user = foundUser; res.status(200); res.json({ messages: 'success' }); return; } /** 若比對不到帳號密碼,就回傳401表示登入失敗 PS:401表示需要授權 */ res.status(401); res.json({ messages: 'user not found. please check your password and account', }); }); ``` 這邊做的API是純API形式,如果以MVC架構來說應該是跳轉頁面等等的操作才對。 不過這不影響,差別只差在失敗以及成功後要執行什麼步驟而已。 2. 第二步 設定判斷及阻擋 ``` const accessMiddleware = (req, res, next) => { /** 若有登入過則直接通過middleware */ if(req.session.user){ next(); return; } /** 沒有則阻擋並回傳錯誤訊息 */ res.status(404); res.json({messages: 'No permission please log in.'}); }; /** 讓請求可以跨域 */ app.use(cors()); /** 將取到的POSR資料轉為JSON PS:如此就不必使用body-parser */ app.use(express.json()); /** session設定 */ app.use(session({ secret: 'node', resave: true, // 設定每次的request都會覆蓋舊server儲存的session存檔。 saveUninitialized: true, cookie:{ maxAge: 10 * 60 * 1000 // 單位是毫秒。1000毫秒 = 1秒,而10分鐘等於 60秒 * 10 } })); /** * 將登入權限的middleware設定在系統設定以及登入API之後 */ app.use(accessMiddleware); ``` 如此便完成了。在這邊我們使用middleware進行判斷,只要未登入都會被阻擋。 這邊得注意我們的middleware是放在/login後,不然使用者連登入都沒辦法都入。 當然了也可以在各字API內來判斷,這就得看需求了。 ### 備註 - 這邊做個備註 當請求的是同domain時使用withCredentials = true會有跨域錯誤 如果要解決有兩個方法 - 前端放弃传递cookie信息,withCredentials设置为false, - 后端要设置Access-Control-Allow-Origin为前端的源地址,例如http://localhost:8080,不能是*,而且还要设置header(‘Access-Control-Allow-Credentials: true’); - 因為前端接收stats非200的code會需要使用到trycatch,所以現在許多API的stateCode都固定回傳200, 然後前端依照response中的某個特定key去判斷狀態。 例如response中有個code,當code為1表示成功,為負數則有各種不同的狀態。 - 現在有越來越多專案都使用JWTTOKEN來當登入依據,最主要是因為session的方法使用多台server會有共用問題,並且也不需要綁cookies, ## EJS 樣版引擎 EJS是類似Vue的template,可以像Vue一樣將資料渲染在HTML中。 在express中可以說是標配。那我們就直接開始跟著教學建立ejs吧。 ### 設定EJS 1. express指定樣版引擎 ``` /** * 指定樣版為EJS * @desc set與use的差別在於"整個框架"要吃什麼樣版引擎 * view engine就是樣版引擎 */ app.set('view engine', 'ejs'); ``` 2. 在public同層建立views資料夾以及index.ejs - 預設的樣版引擎位置都是在views底下 3. 先簡單建立一個html標籤在index.ejs中 ``` <h1>this is ejs page</h1> ``` 4. 設定render ``` /** 當請求路徑為ejs時 使用render渲染指定ejs頁面給client端 */ app.get('/ejs', (req, res) => { res.render('index.ejs'); }); ``` 如此完成以上步驟後,請求/ejs網址就可以看到html的畫面了。 ### 標籤 EJS有分成不同的標籤,不同的標籤有不同的功能。 而這些標籤就是將所接收的變數在標籤上做運用。 - <% %> 流程控制使用 if/else/for/while相關功能使用此標籤 example: ``` <$ if(model !== undefined){ %> <ul> <% for(var i = 0; i < model.items.length; i++) {%> <li><%= model.items[i].name %></li> <% } %> </ul>`; <% }else{ %> <div> 沒有此model </div> <% } %> ``` 從範例中可以看得到,ejs不算html標籤的一種,它只是用語法將html給包起來 - <%= %> 顯示、輸出變數使用 會直接渲染純文本 ``` <h1><%= title %></h1> ``` - <%- %> 渲染html標籤 通常作為導入compoent使用 ``` <%- include('footer.ejs') %> ``` PS: include也可以帶入參數 <%- include('footer.ejs', {...}) %> - <%# %> 註解用 example: ``` <%# 註解程式碼 %> ``` 以上就是EJS的標籤,在範例中有model、title等變數,這些變數就是我們在res.render中所傳入的變數。 當然我們剛才的範例中並沒有傳入變數,只是簡單的顯示EJS的頁面。 所以現在我們開始傳入變數並且運用吧! ### 傳入資訊及運用 一樣延續上面建立EJS的設定 1. render時傳入資料 ``` app.get('/ejs', (req, res) => { res.render('index.ejs', { title: 'ejs title', list: [ { name: 'George', age: '25' }, { name: 'Amy', age: '33' }, ] }); }); ``` 2. ejs檔加入接收資料以及對應的處理 ``` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><%= title %></title> </head> <body> <% if (list.length > 0) { %> <ul> <% for(var i = 0; i < list.length; i++) { %> <li><%= list[i].name %></li> <% } %> </ul> <% } else { %> <div>沒有資料</div> <% } %> <%# 引入footer %> <%- include('footer.ejs') %> </body> </html> ``` 如此便完成了基本的EJS。 **補充: 如果要引入其他ejs只要在views建立要引入的ejs檔即可** **補充: 如果改變的是靜態檔案,nodemon是不會更新的。** ## 設計API 雖然我們會寫API了,但API規劃及設計也是一個重要的技能,包含API該怎麼模組化、該怎麼取資料等等。 一般來說,API會根據資料庫來決定要如何去設計。 而資料庫最簡單的設計方式就是依照功能。 ## MongoDB ## MYSQL 突破了重重難關,我們學會了基本的node.js終於來到了資料庫的關卡啦 (撒花 SQL主要是使用關聯式資料庫。 ### 什麼是關聯式資料庫(RDBMS)? 我們拿excel來說明好了,關聯式資料庫就像是一個excel檔案,裡面有許多的頁籤,一個頁籤中有一個表格。 其中裡面有一個員工資料的頁籤,一個則是職位表的頁籤。 而我們知道職位表中的每個職位都是唯一的。 例如職位是課長,那我們在表中只會設定一個課長,除非課長還有細分,不然就只會設定一個課長。 這個課長在**這張表**中就會有屬於自己專門的id,也叫做主鍵。 此時來到員工資料的頁籤,每個員工的編號都是唯一的,這是這張表單的主鍵。 而員工都會有職位,假設A員工的職位是課長, 那我們就會在這張表格中,這個員工的資料部分填上職位表上課長的ID,此時因為主鍵在其他張表格中,所以課長的ID為外鍵 **補充: 主鍵(primary key) 外鍵(foreign key)** ### 關聯式的特色 - 資料遵行交易原則,新增只能全有或是全無,舉個例子來說表單的欄位有姓名、性別、職位,那在新增時就得所有資料都新增,不能只新增部分欄位。 - 資料庫有ACID四個原則 - 不可分割性 資料在異動時需要全部的流程都完成,不然會回到原始的狀態。 拿上方的例子就是我在新增三個欄位時,需要三個欄位都新增成功,不允許在新增三個欄位時只有部分欄位新增成功的狀況。 - 一至性 資料異動時需要根據當初所設定的規則下去做異動,符合則提交變更(Commit),如果途中不符合則一樣會回到初始狀態(Rollback) 這是為了保證資料的嚴謹。 - 獨立性 不會讓兩筆交易同時進行,避免Race Condition的狀況出現。 舉個例子。如果甲乙兩人同時查詢同一班機同一座位,兩人都發現有此座位後進行訂購,甲先完成付款後乙也完成付款,就會造成甲拿到座位而乙付了錢沒有拿到座位,這就是Race Condition的情況,所以I(Isolation)隔離性會透過SELECT … FOR UPDATE的語法對座位進行Lock機制(鎖定),讓甲先完成交易,乙只能等甲完成交易後得知座位數量在進行交易,來確保資料不會在同時間被改動。 - 持久性 除非使用commit去修改數據,否則資料都是不會異動的,除非硬體受損否則資料永遠不會流失。 即使在資料寫入的當下當機引發寫入時的資料流失,RDBMS也有機制在之後復原資料。 **補充: 所謂交易就是對資料庫下的動作指令** ### 資料庫物件名稱 - 表格(table) 關聯式資料庫的基本儲存格式。 表格中每筆資料(row)由許多欄位(column)組成,每個欄位可設定不同資料型態及其大小 - 視景(view) 可以整合不同的表格讓外部的人去觀看整合後的資料,可設為一個表格並且不會佔儲存空間。 可以視為是查詢後的結果去顯示。 - 序列(sequence) 序列可將索引0.1.2.3.4.5這種改成指定想要改成的索引,例如A.B.C.D等 - 索引 與陣列的索引差不多,通常查詢時會利用索引到索引之間去查詢及篩選。 舉個例子。1~10000筆的索引查詢。 - 觸發器(trigger) 在新增or刪除等資料異動時時會觸發的程式碼 例如在新增A表格時也需要新增B表格就會用到 - 程序(procedure) 存在資料庫的程式碼 ### 資料庫型別及型態 有做標記的是常用的資料型態。 #### 數字 - TINYINT 有符號的範圍是-128 ~ 127 無符號則是 0 ~ 255 - SMALLINT 有符號 -32768~32767 無符號 0~65535 - MEDIUMINT 有符號 -8388608~8388607 無符號 0~16777215 - **INT/INTGER** 有符號 -2147683648~2147683647 無符號 0~4294967295 - BIGINT 有符號 -9223372036854775808~9223372036854775807 無符號 0~18446744073709551615 - **DECIMAL/NUMERIC** 從-10^38 +1 ~ 10^38 -1 常用 有小數點會使用的資料格式 #### 時間 - **DATE** 從1000-01-01 ~ 9999-12-31 - **DATETIME** 日期時間組合 從1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 - TIMESTAMP 時間戳記 1970~2037年之間的某個時刻 - YEAR 特定時間的年 #### 字串 - CHAR 固定的字串 長度在1 ~ 255 建立後不可更改長度 - **VARCHAR** 固定的字串 長度在1 ~ 255 建立後可更改長度 #### 其他 - LONGBLOB 很多的字或轉換的資料會儲存 - **LONGTEXT** 超過255的字串時使用 - ENUM 待補 - SET 算是賦予的一種,用於資料更新 set column = '1' ### 資料表語法基礎運用 #### 建立表格 格式: CREATE TABLE 表格名稱( 欄位名稱 型別 其他功能 ); example: CREATE TABLE students( ID int primary AUTO_INCERMENT, // 從左到右分別是 欄位名稱 型別 設定主鍵 自動設定主鍵 name varchar(15) not null, // not null 為不可為空 arg varchar(15) not null, email varchar(20) not null, deparment varchar(20) not null, ); **補充: 有primary代表這個欄位是primary key** **補充: AUTO_INCERMENT設定後就算刪除了也不會去補充被刪除的ID。舉個例子刪除2也只會往下新增3** #### 修改表格 格式: ALTER TABLE 表格名稱 動作 - 修改表格名稱 ALTER TABLE 表格名稱 rename to 新表格名稱 - 新增欄位 ALTER TABLE 表格名稱 add column 欄位名稱 型別 - 修改欄位 ALTER TABLE 表格名稱 MODIFY 欄位名稱 型別 - 刪除欄位 ALTER TABLE 表格名稱 drop column 欄位名稱 - 刪除資料表 DROP TABLE 表格名稱 刪除後欄位索引也會跟著刪除,這點須注意。 #### 資料處理-新增資料 格式: INSERT INFO `表格名稱`(`欄位名稱1`, `欄位名稱2`) VALUES(`欄位1的值`, `欄位2的值`) example: INSERT INFO `students`(`name`, `arg`) VALUES(`George`, `18`) #### 資料處理-更新資料 格式: UPDATE 表格名稱 SET column=value WHERE 改變的條件 example: UPDATE students SET name=`George` WHERE ID = 1 #### 資料處理-刪除資料 這邊是刪除資料表中該row的資料 格式: DELETE FROM table // 這邊將表格名稱省略為table [WHERE condition] // 也就是改變的條件 example: DELETE FROM students WHERE ID = 1 #### 資料查詢-擷取 格式: SELECT 欄位名稱 FROM 表格 條件 [WHERE condition] / [GROUP BY group_by_expression] / [HAVEING group_condition] / [ORDER BY{column,expression,alias}[ASC|DESC]] example: 需求:取得在職員工姓名及編號 並使用降冪排列 ``` SELECT NAME EXPRESSION FROM EMPLOYEE WHERE STATE = '在職' ORDER BY DEPARMENTID DESC // order by是排序 ``` 如此以上就會顯示出**在職中**只有員工姓名及編號的降冪資料 **備註:ASC是升冪 DESC為降冪 若不設定排序則預設為ASC** **備註:升冪:小到大 降冪:大到小** 格式說明: - SELECT是資料庫查詢的標準語法 - 欄位名稱可以使用*。指全部的欄位資料 - FROM 就是要查詢的表格 **補充:若無下條件則表格內所有資料都會更新** #### 資料查詢-合併查詢 有時我們需要查詢的資料在A表格沒有,而是在B表格。 但AB表格卻有相關的聯繫,此時就會使用到合併查詢。 舉個例子,A表格為客戶資料,B表格為訂單資料。 這兩者就息息相關了對吧?有客戶才會有訂單。 那這時就會需要用到JOIN,而JOIN有分為幾種不同的方式,讓我們一步一步了解。 - INNER JOIN 只篩選出符合條件的資料 在以下的程式碼中,我們只會顯示customers中的name以及order.order_No的欄位。 主要查詢為customers表格並加入了orders這張表格合併查詢 使用on設定連接的主外鍵,customers.C_Id(外鍵)等於orders.C_Id(主鍵) ``` SELECT customers.Name, orders.Order_No FROM customers INNER JOIN orders ON customers.C_Id=orders.C_Id; ``` - RIGHT JOIN (右邊)加入的表格都會顯示出結果 與上方類似,差別在於不管加入的表格有沒有配對到主查詢表格,全部都會顯示。 按照下方的程式碼,如果主查詢表格有配對到就會顯示, 但沒有配對到主表格的orders.C_Id也會顯示出來,只是原先customers.C_Id則會顯示為null ``` SELECT customers.Name, orders.Order_No FROM customers RIGHT JOIN orders ON customers.C_Id=orders.C_Id; ``` - RIGHT JOIN (左邊)主表格都會顯示出結果 與上方相反。 ``` SELECT customers.Name, orders.Order_No FROM customers LEFT JOIN orders ON customers.C_Id=orders.C_Id; ``` #### 資料查詢-常用語法補充 - HAVING HAVING 子句是用來取代 WHERE 搭配聚合函數 (aggregate function) 進行條件查詢,因為 WHERE 不能與聚合函數一起使用。 聚合函數指的也就是 AVG()、COUNT()、MAX()、MIN()、SUM() 等這些內建函數。 接著看程式碼,以下程式碼是要在orders這張表格中找到price小於1000的資料。 最後使用Customer這個欄位做排序 ``` SELECT Customer, SUM(Price) FROM orders GROUP BY Customer HAVING SUM(Price)<1000; ``` ### 在MYSQL上實際操作 了解語法以後就可以在MYSQL上實際操作,雖然後續我們在寫API時會將SQL語法寫在程式中。 但我們可以先在MYSQL中先練習一下。 流程: 1. 首先使用UI創建DB 2. 左側列表點擊DB到達DB頁面 3. 點選上方SQL按鈕 4. 開始在輸入匡中輸入語法 ### node連接mysql 步驟: 1. 專案中安裝sql ``` yarn add mysql ``` 2. 根目錄新建config資料夾 這個資料夾是專門用來建立與DB連線相關的檔案 3. 在config資料夾建立db.js 4. db.js中依照mysql建立一個連接實例 ``` const mysql = require('mysql'); const dbConnection = mysql.createConnection({ host: 'localhost', // 要連接的sql domain。因為我們是連接本地所以使用localhost user: 'root', password: '', database: 'demo', }); dbConnection.connect((err) => { if (err) throw err; console.log('db is connected'); }); module.exports = dbConnection; ``` 5. 在進入點使用export的檔案 ``` const dbConnection = require('./config/db'); ``` 到以上就算完成與資料庫的連線。因為當引入時就已經執行db.js,所以就不需要再額外執行其他動作。 需要使用的時候再使用引入的dbConnection去做與資料庫連接的動作即可。 6. 使用sql語法與db進行溝通 ``` /** R */ app.get('/getAll', (req, res)=> { /** 這邊注意只有用查找功能時result才會有回應,其他如果是刪除以及新增等是不會有的 */ dbConnection.query('SELECT * FROM students', (err, result)=> { if (err) throw err; // 出錯時回傳錯誤資訊 res.json(result); }); }); /** C */ app.post('/add', (req, res)=> { const { name, age, mail, department } = req.body; /** * 這邊使用帶入參數的方式將value帶入,所以使用的是VALUES(?,?,?,?) * 之所以不使用樣版字面值是因為會有安全性的問題 */ dbConnection.query( 'INSERT INTO students(name, age, mail, department) VALUES(?,?,?,?)', [name, age, mail, department], (err)=> { /** 前面有提到result在非查找是不會有資料的 所以這邊直接傳success */ if (err) throw err; res.send('success'); }); }); /** U */ app.patch('/update', (req, res)=> { const { id,age } = req.body; dbConnection.query( 'update students set age=? where id=?', [age, id], (err)=> { if (err) throw err; res.send('updateSuceess'); }); }); /** D */ app.delete('/delete', (req, res)=> { const { id } = req.body; dbConnection.query( 'delete FROM students where id=?', [id], (err)=> { if (err) throw err; res.json('deleteSuccess'); }); }); ``` ## Express Generator 產生一個最基礎的express架構的功能 ## express中request以及response常用參數及方法 - request.method = 使用者請求的方式 - request.url = 使用者請求的路徑(route) - request.query = 獲取domain後的參數 指的是這個>> url?example=123 - request.body = post請求中所傳來的request資料 - response.redirect = 導向不同路由# Node.js及MongoDB、MYSQL筆記 ### 前言 MongoDB是noSQL也就是非關聯性資料庫,不會像MySQL一樣每一個分頁的表格都互相關聯。 ### 特點 - 格式不固定 只需要JSON即可 - 大量資料與即時性資料可使用 - ## Node.js ### 起手 新建一個node專案,則使用npm init。 當輸入完所需資料後就會幫我們自動新建一個package.json #### 輸入的東西 - package name 專案名稱 - version 版本 - descript 專案描述 - entry point 程式進入點(第一個要讀的入口檔案) - test commend 測試專案要如何測試 - git repository 遠端git倉庫 - keyword 讓其他人在npm中搜尋此專案的關鍵字 - author 作者 - license 使用規範 輸入完成後package.json大概會如下 ``` { "name": "learn-project", // 專案名稱 "version": "0.0.1", // 版本 "description": "just learn", // 描述 "scripts": {}, // 專案指令 "main": "index.js", // 進入點 "author": "me", // 作者 "license": "MIT", // 授權 "private": true, // 是否為隱私 "dependencies": {} // 安裝的套件 } ``` #### 如何啟動npm專案 我們知道要使用node執行一個JS檔案就是輸入 node 檔名.js。 不過我們在使用npm的專案時都是使用 npm run serve / yarn run serve 並沒有使用node 去執行啊? 其實是有使用node的,只不過我們將node的指令封裝載package.json了。 只需要在package.json中的script封裝指令,即可使用npm執行我們所想要執行的動作。 ``` "scripts": { "serve": "node index.js" }, ``` 依照上方的程式碼封裝後,我們只需要輸入npm run serve那就代表我們輸入了node index.js的指令。 至於npm其他的指令這邊就不做多述。 ### 使用node建立http server 在打API時其實是瀏覽器在跟伺服器溝通,如果要寫API自然會需要一個伺服器讓使用可以與我們溝通。 而node本身其實就有自帶建立http server的功能。 以下會一步一步的來建立https server ``` import http from 'http'; const PORT = 3000; /** 建立server */ const server = http.createServer((req, res) => { /** 第一步驟定是先設定header 回傳一定需要有格式等資訊 */ res.writeHead(200, {'Content-Type':'text/html'}); /** 第二步驟 要回傳什麼 */ res.write('you want send content'); /** 第三步驟 回傳的結束 */ res.end(); }); /** * 第四步驟 監聽port * @desc 當port號被請求時執行server所設定的程序 */ server.listen(PORT); ``` #### port號是什麼? port可以說是伺服器對外的接口。 也可以想像他是一把鑰匙,只有持有鑰匙的人才可以進到指定房間。 舉個例子,你想打API請求自然就得去https or http的port處理。 你到了其他的port號是沒有用的。 當然這個port號是由處理的人來定義的。 如果我今天想把http port號定在9453,那我API連9453也是可以請求得到。 常用port號: - https port號 443 - http port號 80 - 21:FTP服務(檔案傳輸) - 23:SSH服務(遠端連接) - 3306:MYSQL服務 - 27017:MongoDB - 3000:前端測試port號 ### 關於API所需要知道的內容 #### client端及server端 我們知道網頁程式有分前端跟後端,那通常怎麼分呢? 其實會以處理API這塊下去做區分。 - client端 又稱前端 client端就是指發送請求以及接收對應請求回傳的那端 - server端 稱為後端 也就是接收請求的伺服器。 server端會根據請求調用資料庫並回傳response給client端 #### http的請求方式 API請求方式總共分為四種,分別是GET、POST、PUT/PATCH、DELETE - GET 與POST並列最常見的請求方式。傳遞資料的方式使用Domain帶參數。 通常用於沒有資安問題以及少量的傳輸 - POST 傳遞方式使用request body,通常資料的傳輸都靠它。 可傳遞大量的資料。 - PUT/PATCH PUT 為修改全部的資料 PATCH 為修改部分資料 - DELETE 顧名思義就是刪除的意思 雖然分成四種,但其實使用POST方式一路到底也可以喔,因為DB資料處理還是要回傳response都是API端在處理的。 所以資料要怎麼處理自然都是看寫出來的程式是如何操作的。過往的方式基本上都是POST一路到底。 在這邊有個專業的術語是CRUD。 C = created = POST / R = read = GET / U = update = PUT/PATCH / D = DELETE **備註: 其實我們只要前往網址,那都算是一種請求。** ## express 在理解完理論基礎以後,我們要來探索實務的部分。 追求開發速度及方便情況下,出現了許多框架與套件。 常見的前端框架有Vue、React等等。 而前端有框架,後端自然也有。 在Node.js中最常見的框架也就是express。 ### 特點 express不只完整的支持API功能,還有許多靈活性。 例如router、server啟動的功能都有支援。官方也有相關的說明。 ### 建立server 請直接看程式碼,接下來會在程式碼中加入註解來說明。 額外的會再進行補充。 ``` /** 首先引入並建立express實例 */ const express = require('express'); const app = express(); /** 設定port號 */ const PORT = 3000; /** 與HTTP建立server一樣,需要去監聽port號。第二個參數為server啟動時會觸發的callback */ app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`) }) ``` ### 建立API server有了,下一步自然就是API了,接下來開始建立我們的API ``` const express = require('express'); const app = express(); const PORT = 3000; /** * 依照請求建立API * @desc 第一個參數為路徑,第二個為要執行的function,可以理解成到指定路徑後要執行的動作 * 這邊的request是client端發送的請求,reponse則是我們server端回傳給client端的response */ app.get('/', (request, response)=>{ /** 設定status code */ response.status(200); /** 設定回傳內容 */ response.send('this is my first API'); }); /** 注意這邊listen一定要放在最後面 可以理解成設定完後依照設定啟動 */ app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` 如此以上就是簡單架設了一個server以及API。 當然,API的架設不可能單單只有這樣,後續會再一步一步的將API所需的知識拼湊起來。 ### 利用express讀取靜態檔案 在實作之前我們得先理解什麼是靜態檔案? 靜態檔案就是指我們不會去更動到的檔案都叫靜態,不管是圖片也好、JS等等的程式碼也好。 我們不會去更改到裡面的內容,統稱為靜態檔案。 舉個例子,圖片。我們只會引入圖片做使用,但我們不會去修改內容。 #### 放置的位置(keyword) 檔案總會需要個資料夾存放,而通常靜態檔案的資料夾名稱會有以下幾種: - assets - static - public #### 實作 理論理解後,我們就可以開始來實作了! ##### 第一步 建立檔案 首先我們要讀取檔案,理所當然會須要檔案。 請在package.json同層中建立一個名為public的資料夾,並在這個資料夾中建立一個images的資料夾。 將圖片放在裡面。 ##### 第二步 設定express.static路徑 ``` const express = require('express'); const app = express(); const PORT = 3000; /** * 掛載絕對路徑下的public * @desc 因為靜態檔案不只是圖片,所以這邊是抓public */ app.use(express.static(__dirname + '/public')); app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` ##### 第三步 確認有設定成功 輸入localhost:3000/images/{你設定的圖片}去查看是否有該圖片 有就代表設定成功。 當然,不只圖片,連HTML檔案等也可以查看喔! PS: 掛載後若要取得圖片,則不需要使用/public/images.png 去取得,因為當設定後自動會將public改為根目錄 ##### 補充: __dirname 絕對路徑 設定路徑時我們需要知道dirname(絕對路徑)是什麼。 絕對路徑就是以根目錄為起點一直到檔案or資料夾在裝置上的位置。 也就是會一路從最根目錄的路徑一路一直到指定的檔案/資料夾的路徑 example: /Users/georgehuang/Desktop/work/node-learn/public/images/pic_betarea.png 至於相對路徑就是只以該檔案/資料夾的那一層為起點,一直到檔案/資料夾的位置 PS: 路徑也可以往回退的 - ./ 當前目錄 - ../ 前一層目錄 - ~ 在mac/linux為家目錄 - @ 在前端框架中代表src ##### 補充: 請求的callback可以有多個 ## express 設計Router 到現在我們已經基本上了解express的基礎功能了, 接下來就是要往進階的方向去探索。例如模組化等等的功能。 畢竟前端都有元件模組化了,後端這個要處理更龐大的資料不可能將處理都塞在同一個檔案中對吧? ### router 路由/路徑的定義 設計之前我們先來理解何謂router。 router是路由又稱為路徑,就是在戳API時的那些/user or /api/test等等的東西。 拿上面我們建立的API來說,我們的路由就是 **'/'**,根據不同的需要我們建立不同的路由來處理對應的功能及需求。 RESTful的API就是透過這些路由以及http協議設計出來的 其定義為:判斷應用程式如何回應用戶端對特定端點的要求,而這個特定端點是一個URL或路徑與特定的http要求方法。 ### express.router() express的router其實就是將前面我們所設計的API模組化。 那我們就開始實作吧! 1. 第一步 建立routes資料夾 這個資料夾就是存放各個模組化router的地方。 在這邊我們在與index.js同層的地方建立一個routes的資料夾,並且在裡面新增一個檔案example.js的檔案 2. 撰寫router ``` const express = require('express'); /** 建立router實例 */ const router = express.Router(); /** * 一樣可以根據請求方式做處理 * 參數的req及res我們知道。就是reqeust及response * next則是middeware的功能,繼續前往下一個function */ router.get('/', (req, res, next) => { const person = [ { name: 'Amy', arg: '33', }, { name: 'George', arg: '25', }, ]; /** 將資料使用JSON格式回傳 */ res.json(person); console.log(next); }); /** 將router使用module的方式export出去 */ module.exports = router; ``` 3. 在入口點引用router ``` const express = require('express'); const app = express(); const PORT = 3000; const exampleRouter = require('./routes/example'); app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); /** * 設定該router路由 * @desc 因為剛才我們在router設定的是'/'根目錄 * 所以當要請求時就會是example/ * 依此類推,只要在router設定的路由都需要依此作延伸 * 例如在router有個路由設定是'/set' * 那我請求的路由就會長'url/example/set' */ app.use('/example',exampleRouter); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` ### statusCode代表的意義 - 200 請求成功 - 201 資源成功被建立 物件建立成功/修改 - 204 server沒做任何回應 不需要回應的功能,例如刪除時可能會是這個 - 301 永久移到新位置 - 302 暫時移到新位置 - 400 錯誤請求 代表request資料錯誤 - 401 未認證 - 403 沒有權限 - 404 找不到資源 資料庫沒有該資料 - 500 伺服器有誤 伺服器有報錯(後端出錯) - 503 伺服器暫無回應 伺服器重啟or部分功能維修中 ### 錯誤訊息的設定 其實錯誤的statusCode也是API設定的。 通常是為了避免出錯後前端有不可預期的顯示。 並且在回傳時也一樣會回傳錯誤訊息。 通常是API再出錯後,會執行error回傳 ``` const express = require('express'); const app = express(); const PORT = 3000; app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); /** 這邊只是做個範例 通常會在判斷後決定要傳送什麼 */ app.get('/error-example', (req, res)=> { res.status(500); const messages = {messages: 'server is sometion wrong'}; res.json(messages); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` ### MVC MVVM架構說明 #### MVC MVC架構中: - controll是負責發送request以及接收response - views 依照controll需求顯示網頁 - model 依照controll需求與DB進行溝通 正常的運行邏輯會是: ``` client端 -> controll跟Model要資料 -> controll拿著資料渲染views ``` #### MVVM MVVM架構中: - view 負責畫面及UI(包括事件的接收與綁定 例如click後 - viewModel 畫面邏輯、事件處理、資料暫存 - model 資料來源 互相資料流 正常運行邏輯: view綁定資料至viewModel -> 資料改變viewModel會去跟model處理資料 -> 處理完成後會回應到view端 or view綁定事件 -> 事件觸發後交由viewModel接手處理 -> viewModel與model交流處理資料 -> 回到viewModel來渲染view ## express middleware ### 什麼是middleware middleware被稱作是中介程式or中介軟體,主要處理request到response之間的事情,例如token的解析、紀錄log等等 當然不一定要在這之間,可以在這之前也可以在之後。 例如token就可能會設計在request之前,先進行身份驗證。 而紀錄log則是在request之後response之前。 ### 實作 在實作之前,我們先提一句,middleware就是一個函式,也是通過use去處理。 它的概念有點難以形容,從程式碼來瞭解就會比較能理解上述的說明。 ``` const express = require('express'); const app = express(); const PORT = 3000; /** 建立middleware 函式 */ const middleware = (req, res, next) => { /** * @desc 這邊紀錄路由、請求方式及時間 * 時間可以使用ISO or UTC統一後,後面要處理也比較方便不會有時區問題 */ console.log(`log:[${req.method}]${req.url}--- ${new Date().toISOString()}`); /** * @desc next是讓請求可以繼續往下執行,如果不加就會在這邊停下 * 是middleware的方法。同時其他的路由處理器也可以使用next() */ next(); }; app.use(express.static(__dirname + '/public')); /** 使用middleware */ app.use(middleware); app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); app.get('/test', (request, response)=>{ response.status(200); response.send('this test API'); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` ### 身份驗證 一般來說身份驗證都是透過JWTTOKEN等等的安全性較高的方式下去判斷。 不過我們現在只是以簡易的方式展現一下身份驗證大概如何處理,所以使用的是query。 query也就是domain後帶的參數。example:url?name=george ``` const express = require('express'); const app = express(); const PORT = 3000; /** 建立middleware 函式 */ const middleware = (req, res, next) => { /** * @desc 這邊紀錄路由、請求方式及時間 * 時間可以使用ISO or UTC統一後,後面要處理也比較方便不會有時區問題 */ console.log(`log:[${req.method}]${req.url}--- ${new Date().toISOString()}`); /** * @desc next是讓請求可以繼續往下執行,如果不加就會在這邊停下 * 是middleware的方法。同時其他的路由處理器也可以使用next() */ next(); }; /** 建立middleware 函式 */ const validationMiddleware = (req, res, next) => { const { query } = req; if(query.name !== 'george'){ const messages = { messages:'你沒有權限登入' }; /** 設定請求失敗的code以及訊息 */ res.status(401); res.json(messages); return; } /** 如果符合則直接往下執行 */ next(); }; app.use(express.static(__dirname + '/public')); /** 使用middleware */ app.use(middleware); /** 使用權限判斷middleware */ app.use(validationMiddleware); app.get('/', (request, response)=>{ response.status(200); response.send('this is my first API'); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` **補充:如果是middleware一般都會命名middleware讓開發者知道這個是用在middleware上** ## express cookies && session處理 在說明以前,得先樹立個觀念,cookies及session並不是要二擇一。 兩個是相補的。 session是處理server端儲存。 cookies則是處理client端儲存。 ### cookies 在實作之前我們先理解cookies是什麼。 cookies通常會由server端發送給client端,然後儲存在瀏覽器。 在取得cookies後,每一次的請求client端都會帶上cookies 算是一種儲存資料的方式。 #### 為什麼會存在cookies? 因為http在發送請求時是不會有狀態這個東西,所以進而才產生。 #### 特點 優: - 只針對特定網域起作用 - 有期限,時間到了自然消失 - 請求時會一併帶入 缺: - 資料儲存在client端,懂使用的可以任一修改 - 資料儲存太多會影響傳輸,因為每次請求都會帶上 **備註: clint端在跨域請求時不會主動帶上cookies 但可以手動設定帶上** ### session session是屬於server端的儲存方式,並且會與cookies搭配。 clent端首次請求時,session會建立獨立的ID並儲存在server端。 同時也會透過response將這個獨立id儲存在前端的cookies中。 那下一次只要clinet端請求時,server端就會根據這個獨立的id來判斷請求的使用者是誰,給予對應的動作。 舉個例子,購物車。 如果是全端處理,那我們可以透過sessionID來判斷使用者是誰,並且顯示他所放入購物車的內容。 **需要注意!這邊的session以及前端的sessionStore不是指同一種東西。** #### 特點 - 存放server端,安全性較高 - 主要用途記錄使用者的的行為與資訊(sessionId是1對1) 例如登入資訊,雖然現在都使用JWTtoken比較多 - session不會消失,只有過期與否 - session通常會包一個cookies出去 #### 運作的流程 1. client端首次API請求 2. 在server端把使用者資訊存入session store,並生成session ID作為索引 3. response時會一併將session ID傳送給使用者並存放在瀏覽器(client端)的cookie 4. 下一次client端API請求時會自動將cookies給帶上 5. server端可以取得此次request中cookies的資料,然後與server端的session進行判斷及處理 #### 儲存的位置 產生的sessionId會儲存在以下位置 - 記憶體 - 資料庫 #### 實作 1. 首先得安裝express-session 2. 引入並使用 ``` const express = require('express'); const app = express(); const session = require('express-session'); // 引入session const PORT = 3000; /** * 設定session * @desc express-session是middleware,所以必須把它寫在路由之前。 * 把express-session寫在路由之前,所有的request都會生成一個session並可以透過req.session這個變數來取得session內容, * 以及req.sessionID來取得session ID。可以在後端用console log來觀察這些變數 */ app.use(session({ secret: 'node', resave: true, // 設定每次的request都會覆蓋舊server儲存的session存檔。 saveUninitialized: true, cookie:{ maxAge: 10 * 60 * 1000 // 單位是毫秒。1000毫秒 = 1秒,而10分鐘等於 60秒 * 10 } })); app.get('/', (req, res)=>{ /** * 設定session並且儲存在session store中 * @desc session後的key可以自行命名 */ req.session.test = '123'; res.status(200); /** * 在後端session store保存的同時也會透過response將session所產生的資料傳給前端 * 前端會存在cookies中 */ res.send('this homePage API' + req.session.test); }); /** * 倘若client端有cookies資料,那其他API一樣可以取到 * 注意這邊的是"取得"request而不是像上方的"賦予" * 所以這邊是判斷請求的資料中的有沒有這個key及value */ app.get('/test', (req, res) => { if(req.session.test){ res.status(200); res.send(req.session.test); return; } res.status(401); res.send('not found'); }); app.listen(PORT, ()=> { console.log(`server is start on port :${PORT}`); }); ``` **這邊注意,我們所設定的session.test在cookies並不會看到,我們看見的cookies key會是設定的name或是預設的connect.sid。** #### express-session的參數設定 - **secret**:server端產生id的簽章 用於產生唯一值地方 - **name**: 存在client端的cookies key名稱 預設 connect.sid session是儲存在後端沒錯,但前端是儲存在cookies,兩者是相互運用。 - **resave**: 每次requst是否覆蓋舊的session - **saveUninitialized**: 保存初始化的session。可以理解為強制將未初始化的session存回 session store,未初始化的意思是它是新的而且未被修改。 這個如果設定是true,會把「還沒修改過的」session就存進session store。以登入的例子來說,就是使用者還沒登入,我還沒把使用者資訊寫入session,就先存放了session在session store。設定為false可以避免存放太多空的session進入session store。另外,如果設為false,session在還沒被修改前也不會被存入cookie。 - **store**: 儲存session的方式 預設是node記憶體 - **Cookies**: - **path**: cookies存在client端的位置 - **httpOnly**: 設定前端不能用JS訪問 - **secure**: 是否使用https - **maxAge**: 設定cookies存活時間 單位為毫秒 ### 實戰運用 登入機制 這邊會做一個以session搭配cookies做的登入機制,不過因為尚未使用到DB,所以資料會先坐在function中。 1. 第一步 建立登入API ``` app.post('/login', (req, res) => { const fackDB = [ { account: 'george123456', password: 'qwqw1212', user: 'george' }, { account: 'amy123456', password: 'qwqw1212', user: 'amy' }, ]; const onCheckLogin = (act, pad) => fackDB.find((user) => user.account === act && user.password === pad); const { account, password } = req.body; const foundUser = onCheckLogin(account, password); /** 若有使用者資訊代表帳密正確 登入成功 回傳成功訊息 */ if(foundUser){ req.session.user = foundUser; res.status(200); res.json({ messages: 'success' }); return; } /** 若比對不到帳號密碼,就回傳401表示登入失敗 PS:401表示需要授權 */ res.status(401); res.json({ messages: 'user not found. please check your password and account', }); }); ``` 這邊做的API是純API形式,如果以MVC架構來說應該是跳轉頁面等等的操作才對。 不過這不影響,差別只差在失敗以及成功後要執行什麼步驟而已。 2. 第二步 設定判斷及阻擋 ``` const accessMiddleware = (req, res, next) => { /** 若有登入過則直接通過middleware */ if(req.session.user){ next(); return; } /** 沒有則阻擋並回傳錯誤訊息 */ res.status(404); res.json({messages: 'No permission please log in.'}); }; /** 讓請求可以跨域 */ app.use(cors()); /** 將取到的POSR資料轉為JSON PS:如此就不必使用body-parser */ app.use(express.json()); /** session設定 */ app.use(session({ secret: 'node', resave: true, // 設定每次的request都會覆蓋舊server儲存的session存檔。 saveUninitialized: true, cookie:{ maxAge: 10 * 60 * 1000 // 單位是毫秒。1000毫秒 = 1秒,而10分鐘等於 60秒 * 10 } })); /** * 將登入權限的middleware設定在系統設定以及登入API之後 */ app.use(accessMiddleware); ``` 如此便完成了。在這邊我們使用middleware進行判斷,只要未登入都會被阻擋。 這邊得注意我們的middleware是放在/login後,不然使用者連登入都沒辦法都入。 當然了也可以在各字API內來判斷,這就得看需求了。 ### 備註 - 這邊做個備註 當請求的是同domain時使用withCredentials = true會有跨域錯誤 如果要解決有兩個方法 - 前端放弃传递cookie信息,withCredentials设置为false, - 后端要设置Access-Control-Allow-Origin为前端的源地址,例如http://localhost:8080,不能是*,而且还要设置header(‘Access-Control-Allow-Credentials: true’); - 因為前端接收stats非200的code會需要使用到trycatch,所以現在許多API的stateCode都固定回傳200, 然後前端依照response中的某個特定key去判斷狀態。 例如response中有個code,當code為1表示成功,為負數則有各種不同的狀態。 - 現在有越來越多專案都使用JWTTOKEN來當登入依據,最主要是因為session的方法使用多台server會有共用問題,並且也不需要綁cookies, ## EJS 樣版引擎 EJS是類似Vue的template,可以像Vue一樣將資料渲染在HTML中。 在express中可以說是標配。那我們就直接開始跟著教學建立ejs吧。 ### 設定EJS 1. express指定樣版引擎 ``` /** * 指定樣版為EJS * @desc set與use的差別在於"整個框架"要吃什麼樣版引擎 * view engine就是樣版引擎 */ app.set('view engine', 'ejs'); ``` 2. 在public同層建立views資料夾以及index.ejs - 預設的樣版引擎位置都是在views底下 3. 先簡單建立一個html標籤在index.ejs中 ``` <h1>this is ejs page</h1> ``` 4. 設定render ``` /** 當請求路徑為ejs時 使用render渲染指定ejs頁面給client端 */ app.get('/ejs', (req, res) => { res.render('index.ejs'); }); ``` 如此完成以上步驟後,請求/ejs網址就可以看到html的畫面了。 ### 標籤 EJS有分成不同的標籤,不同的標籤有不同的功能。 而這些標籤就是將所接收的變數在標籤上做運用。 - <% %> 流程控制使用 if/else/for/while相關功能使用此標籤 example: ``` <$ if(model !== undefined){ %> <ul> <% for(var i = 0; i < model.items.length; i++) {%> <li><%= model.items[i].name %></li> <% } %> </ul>`; <% }else{ %> <div> 沒有此model </div> <% } %> ``` 從範例中可以看得到,ejs不算html標籤的一種,它只是用語法將html給包起來 - <%= %> 顯示、輸出變數使用 會直接渲染純文本 ``` <h1><%= title %></h1> ``` - <%- %> 渲染html標籤 通常作為導入compoent使用 ``` <%- include('footer.ejs') %> ``` PS: include也可以帶入參數 <%- include('footer.ejs', {...}) %> - <%# %> 註解用 example: ``` <%# 註解程式碼 %> ``` 以上就是EJS的標籤,在範例中有model、title等變數,這些變數就是我們在res.render中所傳入的變數。 當然我們剛才的範例中並沒有傳入變數,只是簡單的顯示EJS的頁面。 所以現在我們開始傳入變數並且運用吧! ### 傳入資訊及運用 一樣延續上面建立EJS的設定 1. render時傳入資料 ``` app.get('/ejs', (req, res) => { res.render('index.ejs', { title: 'ejs title', list: [ { name: 'George', age: '25' }, { name: 'Amy', age: '33' }, ] }); }); ``` 2. ejs檔加入接收資料以及對應的處理 ``` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><%= title %></title> </head> <body> <% if (list.length > 0) { %> <ul> <% for(var i = 0; i < list.length; i++) { %> <li><%= list[i].name %></li> <% } %> </ul> <% } else { %> <div>沒有資料</div> <% } %> <%# 引入footer %> <%- include('footer.ejs') %> </body> </html> ``` 如此便完成了基本的EJS。 **補充: 如果要引入其他ejs只要在views建立要引入的ejs檔即可** **補充: 如果改變的是靜態檔案,nodemon是不會更新的。** ## 設計API 雖然我們會寫API了,但API規劃及設計也是一個重要的技能,包含API該怎麼模組化、該怎麼取資料等等。 一般來說,API會根據資料庫來決定要如何去設計。 而資料庫最簡單的設計方式就是依照功能。 ## MongoDB ## MYSQL 突破了重重難關,我們學會了基本的node.js終於來到了資料庫的關卡啦 (撒花 SQL主要是使用關聯式資料庫。 ### 什麼是關聯式資料庫(RDBMS)? 我們拿excel來說明好了,關聯式資料庫就像是一個excel檔案,裡面有許多的頁籤,一個頁籤中有一個表格。 其中裡面有一個員工資料的頁籤,一個則是職位表的頁籤。 而我們知道職位表中的每個職位都是唯一的。 例如職位是課長,那我們在表中只會設定一個課長,除非課長還有細分,不然就只會設定一個課長。 這個課長在**這張表**中就會有屬於自己專門的id,也叫做主鍵。 此時來到員工資料的頁籤,每個員工的編號都是唯一的,這是這張表單的主鍵。 而員工都會有職位,假設A員工的職位是課長, 那我們就會在這張表格中,這個員工的資料部分填上職位表上課長的ID,此時因為主鍵在其他張表格中,所以課長的ID為外鍵 **補充: 主鍵(primary key) 外鍵(foreign key)** ### 關聯式的特色 - 資料遵行交易原則,新增只能全有或是全無,舉個例子來說表單的欄位有姓名、性別、職位,那在新增時就得所有資料都新增,不能只新增部分欄位。 - 資料庫有ACID四個原則 - 不可分割性 資料在異動時需要全部的流程都完成,不然會回到原始的狀態。 拿上方的例子就是我在新增三個欄位時,需要三個欄位都新增成功,不允許在新增三個欄位時只有部分欄位新增成功的狀況。 - 一至性 資料異動時需要根據當初所設定的規則下去做異動,符合則提交變更(Commit),如果途中不符合則一樣會回到初始狀態(Rollback) 這是為了保證資料的嚴謹。 - 獨立性 不會讓兩筆交易同時進行,避免Race Condition的狀況出現。 舉個例子。如果甲乙兩人同時查詢同一班機同一座位,兩人都發現有此座位後進行訂購,甲先完成付款後乙也完成付款,就會造成甲拿到座位而乙付了錢沒有拿到座位,這就是Race Condition的情況,所以I(Isolation)隔離性會透過SELECT … FOR UPDATE的語法對座位進行Lock機制(鎖定),讓甲先完成交易,乙只能等甲完成交易後得知座位數量在進行交易,來確保資料不會在同時間被改動。 - 持久性 除非使用commit去修改數據,否則資料都是不會異動的,除非硬體受損否則資料永遠不會流失。 即使在資料寫入的當下當機引發寫入時的資料流失,RDBMS也有機制在之後復原資料。 **補充: 所謂交易就是對資料庫下的動作指令** ### 資料庫物件名稱 - 表格(table) 關聯式資料庫的基本儲存格式。 表格中每筆資料(row)由許多欄位(column)組成,每個欄位可設定不同資料型態及其大小 - 視景(view) 可以整合不同的表格讓外部的人去觀看整合後的資料,可設為一個表格並且不會佔儲存空間。 可以視為是查詢後的結果去顯示。 - 序列(sequence) 序列可將索引0.1.2.3.4.5這種改成指定想要改成的索引,例如A.B.C.D等 - 索引 與陣列的索引差不多,通常查詢時會利用索引到索引之間去查詢及篩選。 舉個例子。1~10000筆的索引查詢。 - 觸發器(trigger) 在新增or刪除等資料異動時時會觸發的程式碼 例如在新增A表格時也需要新增B表格就會用到 - 程序(procedure) 存在資料庫的程式碼 ### 資料庫型別及型態 有做標記的是常用的資料型態。 #### 數字 - TINYINT 有符號的範圍是-128 ~ 127 無符號則是 0 ~ 255 - SMALLINT 有符號 -32768~32767 無符號 0~65535 - MEDIUMINT 有符號 -8388608~8388607 無符號 0~16777215 - **INT/INTGER** 有符號 -2147683648~2147683647 無符號 0~4294967295 - BIGINT 有符號 -9223372036854775808~9223372036854775807 無符號 0~18446744073709551615 - **DECIMAL/NUMERIC** 從-10^38 +1 ~ 10^38 -1 常用 有小數點會使用的資料格式 #### 時間 - **DATE** 從1000-01-01 ~ 9999-12-31 - **DATETIME** 日期時間組合 從1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 - TIMESTAMP 時間戳記 1970~2037年之間的某個時刻 - YEAR 特定時間的年 #### 字串 - CHAR 固定的字串 長度在1 ~ 255 建立後不可更改長度 - **VARCHAR** 固定的字串 長度在1 ~ 255 建立後可更改長度 #### 其他 - LONGBLOB 很多的字或轉換的資料會儲存 - **LONGTEXT** 超過255的字串時使用 - ENUM 待補 - SET 算是賦予的一種,用於資料更新 set column = '1' ### 資料表語法基礎運用 #### 建立表格 格式: CREATE TABLE 表格名稱( 欄位名稱 型別 其他功能 ); example: CREATE TABLE students( ID int primary AUTO_INCERMENT, // 從左到右分別是 欄位名稱 型別 設定主鍵 自動設定主鍵 name varchar(15) not null, // not null 為不可為空 arg varchar(15) not null, email varchar(20) not null, deparment varchar(20) not null, ); **補充: 有primary代表這個欄位是primary key** **補充: AUTO_INCERMENT設定後就算刪除了也不會去補充被刪除的ID。舉個例子刪除2也只會往下新增3** #### 修改表格 格式: ALTER TABLE 表格名稱 動作 - 修改表格名稱 ALTER TABLE 表格名稱 rename to 新表格名稱 - 新增欄位 ALTER TABLE 表格名稱 add column 欄位名稱 型別 - 修改欄位 ALTER TABLE 表格名稱 MODIFY 欄位名稱 型別 - 刪除欄位 ALTER TABLE 表格名稱 drop column 欄位名稱 - 刪除資料表 DROP TABLE 表格名稱 刪除後欄位索引也會跟著刪除,這點須注意。 #### 資料處理-新增資料 格式: INSERT INFO `表格名稱`(`欄位名稱1`, `欄位名稱2`) VALUES(`欄位1的值`, `欄位2的值`) example: INSERT INFO `students`(`name`, `arg`) VALUES(`George`, `18`) #### 資料處理-更新資料 格式: UPDATE 表格名稱 SET column=value WHERE 改變的條件 example: UPDATE students SET name=`George` WHERE ID = 1 #### 資料處理-刪除資料 這邊是刪除資料表中該row的資料 格式: DELETE FROM table // 這邊將表格名稱省略為table [WHERE condition] // 也就是改變的條件 example: DELETE FROM students WHERE ID = 1 #### 資料查詢-擷取 格式: SELECT 欄位名稱 FROM 表格 條件 [WHERE condition] / [GROUP BY group_by_expression] / [HAVEING group_condition] / [ORDER BY{column,expression,alias}[ASC|DESC]] example: 需求:取得在職員工姓名及編號 並使用降冪排列 ``` SELECT NAME EXPRESSION FROM EMPLOYEE WHERE STATE = '在職' ORDER BY DEPARMENTID DESC // order by是排序 ``` 如此以上就會顯示出**在職中**只有員工姓名及編號的降冪資料 **備註:ASC是升冪 DESC為降冪 若不設定排序則預設為ASC** **備註:升冪:小到大 降冪:大到小** 格式說明: - SELECT是資料庫查詢的標準語法 - 欄位名稱可以使用*。指全部的欄位資料 - FROM 就是要查詢的表格 **補充:若無下條件則表格內所有資料都會更新** #### 資料查詢-合併查詢 有時我們需要查詢的資料在A表格沒有,而是在B表格。 但AB表格卻有相關的聯繫,此時就會使用到合併查詢。 舉個例子,A表格為客戶資料,B表格為訂單資料。 這兩者就息息相關了對吧?有客戶才會有訂單。 那這時就會需要用到JOIN,而JOIN有分為幾種不同的方式,讓我們一步一步了解。 - INNER JOIN 只篩選出符合條件的資料 在以下的程式碼中,我們只會顯示customers中的name以及order.order_No的欄位。 主要查詢為customers表格並加入了orders這張表格合併查詢 使用on設定連接的主外鍵,customers.C_Id(外鍵)等於orders.C_Id(主鍵) ``` SELECT customers.Name, orders.Order_No FROM customers INNER JOIN orders ON customers.C_Id=orders.C_Id; ``` - RIGHT JOIN (右邊)加入的表格都會顯示出結果 與上方類似,差別在於不管加入的表格有沒有配對到主查詢表格,全部都會顯示。 按照下方的程式碼,如果主查詢表格有配對到就會顯示, 但沒有配對到主表格的orders.C_Id也會顯示出來,只是原先customers.C_Id則會顯示為null ``` SELECT customers.Name, orders.Order_No FROM customers RIGHT JOIN orders ON customers.C_Id=orders.C_Id; ``` - RIGHT JOIN (左邊)主表格都會顯示出結果 與上方相反。 ``` SELECT customers.Name, orders.Order_No FROM customers LEFT JOIN orders ON customers.C_Id=orders.C_Id; ``` #### 資料查詢-常用語法補充 - HAVING HAVING 子句是用來取代 WHERE 搭配聚合函數 (aggregate function) 進行條件查詢,因為 WHERE 不能與聚合函數一起使用。 聚合函數指的也就是 AVG()、COUNT()、MAX()、MIN()、SUM() 等這些內建函數。 接著看程式碼,以下程式碼是要在orders這張表格中找到price小於1000的資料。 最後使用Customer這個欄位做排序 ``` SELECT Customer, SUM(Price) FROM orders GROUP BY Customer HAVING SUM(Price)<1000; ``` ### 在MYSQL上實際操作 了解語法以後就可以在MYSQL上實際操作,雖然後續我們在寫API時會將SQL語法寫在程式中。 但我們可以先在MYSQL中先練習一下。 流程: 1. 首先使用UI創建DB 2. 左側列表點擊DB到達DB頁面 3. 點選上方SQL按鈕 4. 開始在輸入匡中輸入語法 ### node連接mysql 步驟: 1. 專案中安裝sql ``` yarn add mysql ``` 2. 根目錄新建config資料夾 這個資料夾是專門用來建立與DB連線相關的檔案 3. 在config資料夾建立db.js 4. db.js中依照mysql建立一個連接實例 ``` const mysql = require('mysql'); const dbConnection = mysql.createConnection({ host: 'localhost', // 要連接的sql domain。因為我們是連接本地所以使用localhost user: 'root', password: '', database: 'demo', }); dbConnection.connect((err) => { if (err) throw err; console.log('db is connected'); }); module.exports = dbConnection; ``` 5. 在進入點使用export的檔案 ``` const dbConnection = require('./config/db'); ``` 到以上就算完成與資料庫的連線。因為當引入時就已經執行db.js,所以就不需要再額外執行其他動作。 需要使用的時候再使用引入的dbConnection去做與資料庫連接的動作即可。 6. 使用sql語法與db進行溝通 ``` /** R */ app.get('/getAll', (req, res)=> { /** 這邊注意只有用查找功能時result才會有回應,其他如果是刪除以及新增等是不會有的 */ dbConnection.query('SELECT * FROM students', (err, result)=> { if (err) throw err; // 出錯時回傳錯誤資訊 res.json(result); }); }); /** C */ app.post('/add', (req, res)=> { const { name, age, mail, department } = req.body; /** * 這邊使用帶入參數的方式將value帶入,所以使用的是VALUES(?,?,?,?) * 之所以不使用樣版字面值是因為會有安全性的問題 */ dbConnection.query( 'INSERT INTO students(name, age, mail, department) VALUES(?,?,?,?)', [name, age, mail, department], (err)=> { /** 前面有提到result在非查找是不會有資料的 所以這邊直接傳success */ if (err) throw err; res.send('success'); }); }); /** U */ app.patch('/update', (req, res)=> { const { id,age } = req.body; dbConnection.query( 'update students set age=? where id=?', [age, id], (err)=> { if (err) throw err; res.send('updateSuceess'); }); }); /** D */ app.delete('/delete', (req, res)=> { const { id } = req.body; dbConnection.query( 'delete FROM students where id=?', [id], (err)=> { if (err) throw err; res.json('deleteSuccess'); }); }); ``` ## Express Generator 產生一個最基礎的express架構的功能 ## express中request以及response常用參數及方法 - request.method = 使用者請求的方式 - request.url = 使用者請求的路徑(route) - request.query = 獲取domain後的參數 指的是這個>> url?example=123 - request.body = post請求中所傳來的request資料 - response.redirect = 導向不同路由