Node.js === ###### tags: `web` `backend` # Node.js 尚未筆記 1. ejs 2. body-parser 3. redirect 4. express + firebase 5. cookie + session 6. email csurf env ## 網站 Node.js : https://nodejs.org/en/ npm : https://www.npmjs.com/ npm 為 nodejs 的套件管理工具,安裝 nodejs 時預設安裝 ## 常用指令 查看版本 ```javascript= node -v npm -v ``` 開始 ```javascript= npm init ``` ``` package.json { "name": "hbdoy", "version": "1.0.0", "description": "test", "main": "test.js", "scripts": { "gogo": "node test.js", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "hbdoy", "license": "ISC", "dependencies": { "jquery": "^3.2.1", "router": "^1.3.0" } } ``` 自訂 & 執行 script ``` "gogo": "node test.js" npm run gogo ``` 版本號 ``` "router": "^1.3.0" 1: 主要版本號(重大更新) 3: 次要版本號(小功能更新) 0: bug更新 ^: 只更新次要版本號以及bug,不貿然更新主要版本號 ~: 只更新bug ``` 執行 JS ```javascript= node myJsCode.js ``` 進入可以直接輸入js執行介面(類似chrome的console) ```javascript= node // 離開: ctrl+c 兩次 ``` 安裝其他 library ```javascript= // 要 save 才會把使用的 library 名稱存到 package.json npm install jquery --save // save 是 node 應用程式上線時會用到的相依套件,若只是要在開發時用來測試的套件,則加上 -dev npm install test --save-dev // C:/user/[使用者名稱]/AppData/Roming/npm/node_modules // 這不會寫入 package.json,而是本機上所有專案都可以使用 npm install test -g ``` 解除安裝 ```javascript= npm uninstall jquery [-g] ``` 顯示安裝的套件 ```javascript= npm list ``` 查看有沒有過期的 library ```javascript= npm outdated ``` 把使用到的library裝回來(讀取於 package.json 中的 dependencies) ```javascript= npm install ``` ### 模組化之間的 data 傳遞 main.js ```javascript= var content = require('./child'); var a = 1; console.log(a); // 如果是物件的話就 content.b就好 console.log(content); // 2 ``` child.js ```javascript= // 商業邏輯可以寫在這裡,只傳出運算後的值 var b = 2; // 也可以傳出物件 module.exports = b; ``` ### Debug node 內建 debug 模式,能夠逐步執行,也可以跳到中斷點(debugger)。 test.js ```javascript= var a = 1; var b = 2; debugger; var c = 3; debugger; ``` 在終端機輸入 ```javascript= node debug test.js ``` 就會進入 debug 模式,並停在第一行,如果要執行下一行就打 ``n`` (next), 如果要跳到最近的 debugger 點就打 ``c`` (cont), 當想要輸入一些指令時(ex: console),就打 ``repl``,就會進入類似 chrome 中 console 的模式。 #### 結合 chrome debug 若是想更圖形化、直覺一點的 debug,可以在終端機輸入 ```javascript= node --inspect --debug-brk test.js ``` 就會得到一串指令,只要把這段指令丟到瀏覽器執行,就會自動開啟該檔案的 debug 工具。 ## Other Library ### browser & server browser | request(ex:get)=> <= response(ex:html) | webserver(ex:apache) 瀏覽器發出 request ,需要有 webserver(httpserver) 才能夠攔截到,並做出回應。 還有若是請求一個php http://hello/index.php server 端無果沒有安裝 php 的模組則會回傳 index.php 的原始碼回來 Nodejs 不需要有 webserver 就能夠對 request 做出回應 ### Node.js HTTP Server server 啟動後,訪問網址時,NodeJs 就能夠做出回應。 ```javascript= var http = require('http'); var hostname = '127.0.0.1'; var port = 7774; http.createServer(function(request, response){ response.writeHead(200); response.write('Hello World'); response.end(); }).listen(port, hostname); ``` ### HTTP Server & File System ```javascript= var http = require('http'); var port = 7774; var hostname = '127.0.0.1'; var fs = require('fs'); http.createServer(function (request, response){ response.writeHead(200, {'Content-Type': 'text/html'}); fs.readFile('index.html', function(error, content){ response.write(content); response.write('Hello World'); response.end(); }); }).listen(port, hostname); ``` ### request library 讓 NodeJS 能夠發出一個 request ``` npm install request --save ``` ```javascript= var request = require('request'); request.get( { url:'https://bot.moli.rocks/ncnu-staff-contact/俞', }, function(err, httpResponse, body){ console.log(`err: ${err}`); console.log(`httpRes: ${httpResponse}`); console.log(`body: ${body}`); }); ``` #### request用於表單 流程為 user 訪問網址 > NodeJS 發出一個 request 到指定網址,然後再把得到的 response 渲染到 browser。 ```javascript= var http = require('http'); var port = 7774; var hostname = '127.0.0.1'; var request = require('request'); http.createServer(function(req, res){ res.writeHead(200, {'Content-Type': 'text/html'}); request.post('http://ycchen.im.ncnu.edu.tw/join.php', { form:{ name: 'LeeRay', sex: 'male', isHandsome: 'Yes' }}, function(err, httpResponse, body){ res.write(body); res.end(); }); }).listen(port, hostname); ``` ### express 一個方便做路由的 web 框架 ``` npm install express --save ``` ``helloWorld.js`` : ```javascript= var express = require('express'); var app = express(); var app2 = express(); app.get('/', function(req, res){ res.send('Hello Get!'); }); app.get('/users', function(req, res){ res.send('Hello users!'); }); app.post('/', function(req, res){ res.send('Hello Post!'); }); app2.get('/', function(req, res){ res.send('Hello 3001_Get!'); }); app.listen(3000, function(){ console.log('http://localhost:3000'); }); app2.listen(3001, function(){ console.log('http://localhost:3001'); }); ``` run ``` node helloWorld.js ``` ``> localhost:3000/users`` #### Router 透過 module 做大分類的路由,此例中只要是到 ``/test`` 下,就是交由 ``users.js`` 做進一步的路由。 像是學校網站可能會在大分類有 ``/teacher`` 和 ``/student``,然後再轉交給各自的路由進行下一步的動作。 ``module_test.js`` : ```javascript= var express = require('express'); var users = require('./users'); var app = express(); // xxx.use 是在指定 URI 時,把路由交給指定 js 做近一步功能 app.use('/test', users); app.listen(3001, function(){ console.log('http://localhost:3001'); }); ``` ``users.js`` : ```javascript= var express = require('express'); var router = express.Router(); // xxx.get 是對指定 method、URL 做動作 router.get('/', function(req, res){ res.send('Get 1,2'); }); router.get('/1', function(req, res){ res.send('Get 1'); }); router.get('/2', function(req, res){ res.send('Get 2'); }); module.exports = router; ``` run : ``` node module_test.js ``` ``` > localhost:3001/test > localhost:3001/test/1 > localhost:3001/test/2 ``` #### middleware 1. 可以用來驗證程式是否要繼續往下執行 ```javascript= var express = require('express'); var app = express(); app.get('/',function(req, res){ res.send('<html><head></head><body><h1>Hello World</h1></body></html>') }) // /user 被訪問時,會先判斷 next 是否執行,才會繼續往下走,否則會卡在 console 那一行 app.use(function(req, res, next){ console.log('有人進來了'); next(); }) app.get('/user',function(req, res){ res.send('<html><head></head><body><h1>我是認證過的user</h1></body></html>') }) // 監聽 port var port = process.env.PORT || 3000; app.listen(port); ``` 也可以寫成 ```javascript= var login = function(req, res, next){ console.log('有人進來了'); next(); }; app.use(login); ``` 如果想要針對某個路由設定驗證就好,可以這樣寫 ```javascript= var login = function(req, res, next){ console.log('有人進來了'); next(); }; app.get('/', login, function(req, res){ res.send('<html><head></head><body><h1>Hello World</h1></body></html>') }) ``` 2. 404頁面、500頁面 :::info 兩段程式碼都是放在最下面,當所有路由規則都不符合就會顯示404。 而多了一個 err 參數的,則是當程式發生錯誤(ex: 執行未定義函數),就會執行。 ::: ```javascript= app.use(function(req, res, next){ res.status(404).send("抱歉,您的頁面找不到"); }) app.use(function(err, req, res, next){ res.status(500).send("程式發生異常錯誤,請稍後再試"); }) ``` #### 載入靜態檔案 如果在 router 中要使用靜態檔案,像是圖片,需要先指定所有存放靜態檔案的位置,否則會抓取不到。 ``public/images`` ```javascript= var express = require('express'); var app = express(); //增加靜態檔案的路徑 app.use(express.static('public')) app.get('/',function(req,res){ res.send('<html><head></head><body><img src="/images/logo.png"</body></html>') }) // 監聽 port var port = process.env.PORT || 3000; app.listen(port); ``` ### express-generator express-generator 可快速建構一個模板,類似於 MVC(model、view、control)架構,不用再手動建立。 ``` npm install express-generator -g ``` `` express -h`` 可以查看各參數用法 快速產生 ``` express name cd name npm install npm start 或是 mkdir name cd name express -f npm install npm start > localhost:3000 ``` #### 從網址列中取值 ``name/routes/index.js`` ```javascript= var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('index', { title: 'Express' }); }); /* /: 冒號後面表示可以接任意字串 */ router.get('/:name', function(req, res, next) { // 用 .params 取 URI 的值 var myName = req.params.name; res.send('HaHaHa Tomato ' + myName); }); module.exports = router; ``` ``res.render('index', { title: 'Express' })`` 中的 render 可以回傳 view 中的指定頁面,而 title 為變數名稱。 ``> localhost:3000/leeray`` :::info 取路徑名稱、參數 ``` app.get('/:name', function(req, res) { // www.xxx.com/Tom?limit=10 // 用 params 取 URI 的值 var myName = req.params.name; // Tom // 用 query 取參數 var myQuery = req.query; // {limit: 10} var limit = req.query.limit; // 10 }); ``` ::: #### 自訂 在上一個範例中,不管 ``localhost:3000/xxx`` 後面打的是什麼,都會顯示 ``HaHaHa Tomato xxx``,但是可以到 ``app.js`` 中新增路由規則,則此名稱將會是一個新的路由,而不會被視為 ``:name``。 到資料夾中找到 ``app.js``,所有初步規則都設定在這邊。 在裡面加上自訂義的新路由規則,記得新增的規則要放在 index 上面。 ```javascript= var classes = require('./routes/classes'); // 記得 /classes 要放在 / 上面,否則不管網址打什麼還是會被視為 index app.use('/classes', classes); app.use('/', index); ``` ``name/routes/classes.js`` ```javascript= var express = require('express'); var router = express.Router(); /* GET users listing. */ router.get('/', function(req, res, next) { res.send('CLASSSSSSSSSSSSSSSES'); }); router.get('/:class_id', function(req, res, next) { var class_id = req.params.class_id; var myData = { name: 'LaaRay' }; res.send('This is class ' + class_id + JSON.stringify(myData)); }); // 再分一個路由下去 var eggs = require('./eggs/eggs.js'); router.use('/:class_id/eggs', eggs); module.exports = router; ``` ``` // CLASSSSSSSSSSSSSSSES > localhost:3000/classes > localhost:3000/classes/xxx ``` 當然也可以再分下一層路由,也就是 ``localhost:3000/classes/xxx/eggs`` 要注意的是因為 ``/:uid`` 有使用到上一層也就是 ``name/routes/classes.js`` 中的 ``class_id`` 變數,所以要設定 ``mergeParams: true``,讓 ``egg.js`` 能夠使用上層變數。 ``name/routes/eggs/eggs.js`` ```javascript= var express = require('express'); // mergeParams: true 讓上層變數可以使用 var router = express.Router({mergeParams: true}); /* GET users listing. */ router.get('/', function(req, res, next) { res.send('Some eggs'); }); router.get('/:uid', function(req, res, next) { var class_id = req.params.class_id; var uid = req.params.uid; res.send("This is " + class_id + " has " + uid + ' eggs'); }); module.exports = router; ``` ``` // Some eggs > http://localhost:3000/classes/xxx/eggs/ // This is xxx has 123 eggs > http://localhost:3000/classes/xxx/eggs/123 ``` # Firebase 創建專案後,可以到 firebase 首頁點選「將 Firebase 加入您的網路應用程式」,得到下面啟用程式碼 ```javascript= <script src="https://www.gstatic.com/firebasejs/4.9.0/firebase.js"></script> <script> // Initialize Firebase var config = { apiKey: "xxxx", authDomain: "xxxx.firebaseapp.com", databaseURL: "https://xxxx.firebaseio.com", projectId: "xxxx", storageBucket: "xxxx.appspot.com", messagingSenderId: "xxxx" }; firebase.initializeApp(config); </script> ``` :::info 預設寫入權限需要登入驗證,如果只是練習可以修改規則設定 ``` { "rules": { // ".read": "auth != null", // ".write": "auth != null" ".read": true, ".write": true } } ``` ::: ## 寫入 ### 選擇路徑 ``ref()``:尋找資料庫路徑 ```javascript= ref('todos/content'); ``` ``child()``:用法和 ref() 差不多 ```javascript= ref('todos').child('content'); ``` ### set ``set()``:新增資料 ```javascript= // 沒有指定路徑代表根目錄 firebase.database().ref().set('hello world'); // 指定路徑 firebase.database().ref('student1/name').set('John'); ``` :::info firebase 中的資料都為物件形式 ::: ### push set 會把路徑下的資料覆寫,push 則是新增額外一筆,並自動產生對應 key。 ```javascript= var todos = firebase.database().ref('todos'); todos.push({content: '記得寫功課'}); ``` ## 讀取 ### once 只會從 firebase 抓取一次,不會即時更新 ```javascript= // 同樣也可以指定路徑,否則就是讀取根路徑下的所有資料 firebase.database().ref().once('value', function(snapshot){ console.log(snapshot.val()); }); // 讀取 myName 下的名字 var myName = firebase.database().ref('myName'); myName.once('value', function(snapshot){ $('#name').html(snapshot.val()); }); ``` ### on 資料庫中的資料變動會即時更新到網頁上 ```javascript= var myName = firebase.database().ref('myName'); myName.on('value', function(snapshot){ $('#name').html(snapshot.val()); }); ``` #### 顯示 firebase 資料到網頁上 ```htmlmixed= <pre id="content"></pre> ``` ```javascript= firebase.database().ref().on('value', function(snapshot){ // stringify 倒數兩個參數是縮排 document.getElementById('content').textContent = JSON.stringify(snapshot.val(), null, 3); }); ``` ## 刪除 ### remove ```javascript= var todos = firebase.database().ref('todos'); todos.child('content').remove(); ``` ## ToDoList ```htmlmixed= <input id="txt" type="text" placeholder="請輸入內容..."> <input type="button" id="send" value="送出"> <ul id="list"></ul> ``` ```javascript= var txt = document.getElementById('txt'); var send = document.getElementById('send'); var list = document.getElementById('list') // todos var todos = firebase.database().ref('todos'); // 按送出按鈕,可以寫入到資料庫 send.addEventListener('click',function(e){ console.log(txt.value); todos.push({content: txt.value}); }) // 顯示內容出來 todos.on('value',function(snapshot){ var str = ''; var data = snapshot.val(); for(var item in data){ str+='<li data-key="'+ item +'">'+data[item].content+'</li>'; } list.innerHTML = str; }) // 刪除邏輯 list.addEventListener('click',function(e){ if(e.target.nodeName=="LI"){ var key = e.target.dataset.key; todos.child(key).remove(); } }) ``` ## 排序 ### orderByChild 若使用 orderByChild 要用 ``forEach`` 撈出值 ```javascript= var peopleRef = firebase.database().ref('people'); peopleRef.orderByChild('age').once('value', function(snapshot){ snapshot.forEach(function(item){ // 撈出的是 item 的值,如果想看該值的 key 就直接 .key 就好 console.log(item.key); console.log(item.val()); }) }) ``` ## 區間 ### startAt、endAt、equalTo ```javascript= var peopleRef = firebase.database().ref('people'); peopleRef.orderByChild('weight').equalTo(2500).once('value',function(snapshot){ // peopleRef.orderByChild('weight').startAt(2500).endAt(3500).once('value',function(snapshot){ snapshot.forEach(function(item){ console.log(item.key); console.log(item.val()); }) }) ``` ## 限制比數 ### limit 取出第一/最後一筆資料 ```javascript= var peopleRef = firebase.database().ref('people'); peopleRef.orderByChild('weight').startAt(2500).limitToFirst(1).once('value',function(snapshot){ // peopleRef.orderByChild('weight').startAt(2500).limitToLast(1).once('value',function(snapshot){ snapshot.forEach(function(item){ console.log(item.key); console.log(item.val()); }) }) ``` # Restful API URL : 網址 http://test.com/a/b/c URI : 資源存放位置 /a/b/c 網址要可讀、簡潔、可預測 符合REST設計風格的 Web API 稱為 RESTful API 它從以下三個方面進行定義: 1. 直觀簡短的網址(URI):http://example.com/resources/ 2. 傳送的資源:JSON、XML 3. 對資源的操作:利用 http method(比如:POST、GET、PUT、DELETE) ### 網站連線方式 Browser URL Enter > Domain 透過 DNS 解析成 IP > http request > GET /a/b/c HTTP/1.1 Other Method/Action : ``Post`` ``Patch`` ``Put`` ``Delete`` ### 實作練習 [規格表](https://hackmd.io/IYVgxgJgZgzGAMBaApmAjMRAWZAjAbIgBxTKH7wTBHD77ACcDyQA) # ORM > 物件關聯對映(英語:Object Relational Mapping,簡稱ORM),是一種程式設計技術,用於實現物件導向編程語言裡不同類型系統的資料之間的轉換。從效果上說,它其實是創建了一個可在編程語言裡使用的「虛擬物件資料庫」。 簡單來說就是用程式語言以操作物件的方式取代 SQL 指令,來對資料庫進行操作。 ## sequelize [搭配 express 的 orm 套件](https://github.com/sequelize/express-example) ### 用法 ``` npm install express-generator -g # 這邊創建 ejs express -e name cd name && npm install npm start ``` 這邊搭配的資料庫是 sqlite ``` # install ORM , CLI and SQLite dialect npm install --save sequelize sequelize-cli sqlite3 # generate models node_modules/.bin/sequelize init ``` :::danger 超級大坑:在 windows 下,``node_modules/.bin/sequelize init`` 會顯示 ``'node_modules' 不是內部或外部命令、可執行的程式或批次檔``,這是因為在 windows 下,路徑要用 ``\`` 而不是 ``/``。 ::: 輸入完之後就會看到多三個資料夾 1. config(設定 DB 連線資料) 2. models(負責操作 DB 的 js) 3. migrations(紀錄所有操作 DB 的動作) 因為這邊要搭配的 DB 是 sqlite,所以要到 ``config\config.json`` 中把 dialect 的 mysql 改成 sqlite,然後就可以建立 DB 了。 ``` node_modules/.bin/sequelize db:migrate ``` ## 範例 這邊要來寫一個記帳功能,能夠 CRUD。 table 項目有 1. title(ex:買珍珠奶茶) 2. type(ex:drink) 3. cost(ex:50) ### create 先寫一個能夠新增資料進 DB 的功能 ``name\models\account.js`` ```javascript= "use strict"; module.exports = function(sequelize, DataTypes){ var Account = sequelize.define('Account', { title: DataTypes.STRING, type: DataTypes.STRING, cost: DataTypes.STRING }); return Account; } ``` 對應首頁的 ``name/routes/index.js``, ejs 為模板語法,可以傳自訂參數。 ``name/routes/index.js`` ```javascript= var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('index', { title: '記帳囉' }); }); module.exports = router; ``` 首頁外觀直接套 bootstrap 模板,並讓新增消費按鈕連到 ``accounts/create`` 網址。 ``name\views\index.ejs`` ```javascript= <!DOCTYPE html> <html> <head> <title> <%= title %> </title> <link rel='stylesheet' href='/stylesheets/style.css' /> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous"> </head> <body> <nav class="navbar navbar-expand-md navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" href="/">記帳囉</a> <div class="ml-auto my-2 my-lg-0"> <a href="/accounts/create"> <button class="btn btn-outline-success my-2 my-sm-0">新增消費</button> </a> </div> </div> </nav> <main role="main"> <div class="jumbotron"> <div class="container"> <h1 class="display-3">記下生活中的花費</h1> <p>做自己錢包的主人</p> <p> <a class="btn btn-primary btn-lg" href="#" role="button">Learn more &raquo;</a> </p> </div> </div> <div class="container"> <div class="row"> </div> </div> </main> <footer class="container"> <hr> <p>&copy; Company 2017</p> </footer> </body> </html> ``` 新增消費明細的頁面 ``name\views\create_account.ejs`` ```javascript= <!DOCTYPE html> <html> <head> <title>記帳囉</title> <link rel='stylesheet' href='/stylesheets/style.css' /> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous"> </head> <body> <nav class="navbar navbar-expand-md navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" href="/">記帳囉</a> <div class="ml-auto my-2 my-lg-0"> <a href="/accounts/create"> <button class="btn btn-outline-success my-2 my-sm-0">新增消費</button> </a> </div> </div> </nav> <div class="container mt-3"> <div class="row justify-content-center"> <div class="col-md-6"> <h2>新增消費</h2> <form method="POST" action="/accounts/create"> <input class="mb-3 form-control" type="text" name="title" placeholder="請輸入消費名稱"> <select class="mb-3 form-control" name="type"> <option value="eat">食</option> <option value="cloth">衣</option> <option value="home">住</option> <option value="traffic">行</option> <option value="play">育樂</option> </select> <input class="mb-3 form-control" type="number" name="cost" placeholder="請輸入消費金額"> <div class="text-right"> <button class="btn btn-primary" type="submit">送出</button> </div> </form> </div> </div> <hr> </div> <footer class="container"> <p>&copy; Company 2017</p> </footer> </body> </html> ``` 在上面新增明細的頁面中,form 會 post 資料到 /accounts/create 的網址,所以要在 control 中做控制。 記得到 ``app.js`` 中新增 ``accounts`` 的路由規則。 ``name\routes\accounts.js`` ```javascript= var express = require('express'); var router = express.Router(); var models = require('../models'); /* 正常訪問時顯示新增消費的頁面 */ router.get('/create', function(req, res, next) { res.render('create_account'); }); /* 接收表單 POST 來的資料並寫進資料庫 */ router.post('/create', function(req, res, next) { console.log(req.body); models.Account.create({ title: req.body.title, type: req.body.type, cost: req.body.cost }).then(function(){ /* 回首頁 */ res.redirect('/'); }); }); module.exports = router; ``` 寫好之後還需要修改兩個地方,一個是在 ``name\bin\www`` 中新增一個自動與 DB 同步的 function。 ``name\bin\www`` ```javascript= /* 要調用 models 中的功能 */ var models = require('../models'); var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); var server = http.createServer(app); // sync() will create all table if they doesn't exist in database models.sequelize.sync().then(function () { server.listen(port); server.on('error', onError); server.on('listening', onListening); }); ``` 另一個是在 windows 環境下,記得修改 ``name\models\index.js`` 中的反斜線。 ``name\models\index.js`` ```javascript= // 對應的 config.json 為 DB 連線資料 var config = require(__dirname + '/../config/config.json')[env]; ``` 都修改好之後就新增資料試試看,送出表單時,就可以詳細過程了 ``` { title: '吃便當', type: 'eat', cost: '80' } Executing (default): INSERT INTO `Accounts` (`id`,`title`,`type`,`cost`,`createdAt`,`updatedAt`) VALUES (NULL,'吃便當','eat','80','2017 -10-29 19:12:17.226 +00:00','2017-10-29 19:12:17.226 +00:00'); POST /accounts/create 302 68.697 ms - 46 ``` ### read 可以 insert data 之後,就要把 DB 中的資料撈到 ``index.ejs`` 顯示。 因為訪問 ``localhost:3000`` 時,是先交由 routes 中的 ``index.js`` 來做路由判斷進一步動作,所以這邊要做的是撈完資料後回傳,再讓 view 來渲染。 ``name/routes/index.js`` ```javascript= var express = require('express'); var router = express.Router(); var models = require('../models'); /* GET home page. */ router.get('/', function(req, res, next) { /* 撈出 DB 所有資料 */ models.Account.findAll().then(function(accounts){ /* 可以看到撈出的資料 */ console.log(accounts); res.render('index', { title: '記帳囉', accounts: accounts }); }); }); module.exports = router; ``` 接著在主頁中將接收的資料透過模板語法渲染出來,有點像 php 的用法,只是若要顯示變數的 value 時,需要多加一個等號 ``<%= 變數 %>``。 ``name/view/index.ejs`` ```javascript= <main role="main"> <div class="jumbotron"> ... </div> <div class="container"> <div class="row"> <div class="col-12"> <table class="table table-hover"> <tr> <th>明細</th> <th>類型</th> <th>金額</th> <th>操作</th> </tr> <% for(var i = 0; i < accounts.length; i++){ %> <tr> <td><%= accounts[i].title %></td> <td><%= accounts[i].type %></td> <td><%= accounts[i].cost %></td> <td></td> </tr> <% } %> </table> </div> </div> </div> </main> ``` ### update 先修改首頁 ``name/view/index.ejs`` ```javascript= <div class="container"> <div class="row"> <div class="col-12"> <table class="table table-hover"> <tr> <th>消費內容</th> <th>類型</th> <th>金額</th> <th>更改</th> <th>刪除</th> </tr> <% for(var i = 0; i < accounts.length; i++){ %> <tr> <td><a href="accounts/<%= accounts[i].id %>"><%= accounts[i].title %></a></td> <td><%= accounts[i].type %></td> <td><%= accounts[i].cost %></td> <td> <a href="accounts/<%= accounts[i].id %>/update" class="btn btn-warning">更新</a> </td> <td> <!-- 刪除採取 post 較為安全一點 --> <form method="post" action="accounts/<%= accounts[i].id %>/delete"> <button class="btn btn-danger" type="submit">刪除</button> </form> </td> </tr> <% } %> </table> </div> </div> </div> ``` 接著在 ``name/view/`` 中新增一個修改的頁面,和原本輸入的頁面 ``name/view/create_account.ejs`` 一樣,只是要帶入原始值而已。 ``name/view/update_account.ejs`` ```javascript= <div class="container mt-3"> <div class="row justify-content-center"> <div class="col-md-6"> <h2>新增消費</h2> <form method="POST" action="/accounts/<%= account.id %>/update"> <input value="<%= account.title %>" class="mb-3 form-control" type="text" name="title" placeholder="請輸入消費名稱"> <select class="mb-3 form-control" name="type"> <option <%= account.type === 'eat'? 'selected' : '' %> value="eat">食</option> <option <%= account.type === 'cloth'? 'selected' : '' %> value="cloth">衣</option> <option <%= account.type === 'home'? 'selected' : '' %> value="home">住</option> <option <%= account.type === 'traffic'? 'selected' : '' %> value="traffic">行</option> <option <%= account.type === 'play'? 'selected' : '' %> value="play">育樂</option> </select> <input value="<%= account.cost %>" class="mb-3 form-control" type="number" name="cost" placeholder="請輸入消費金額"> <div class="text-right"> <button class="btn btn-primary" type="submit">送出</button> </div> </form> </div> </div> </div> ``` 接著在 ``name/routes/accounts.js`` 中新增 update 動作 ``name/routes/accounts.js`` ```javascript= /* update頁面 */ router.get('/:account_id/update', function(req, res, next) { console.log(req.params); models.Account.findOne({ where: { id: req.params.account_id } }).then(function(account){ res.render('update_account', {account: account}); }); }); /* update DB 中的某筆資料 */ router.post('/:account_id/update', function(req, res, next) { models.Account.findOne({ where: { id: req.params.account_id } }).then(function(account){ account.update({ title: req.body.title, type: req.body.type, cost: req.body.cost }); }).then(function(){ res.redirect('/'); }); }); ``` :::info params 取 URL 中對應參數的值 query 取 GET 時 URL 中的參數(?p=test) body 取表單 POST 過來的值 ::: ### delete 剛剛在 update 中已經在主頁加上刪除的按鈕了,這邊只剩寫入邏輯操作就好。 ``name/routes/accounts.js`` ```javascript= /* delete DB 中的某筆資料 */ router.post('/:account_id/delete', function(req, res, next) { models.Account.destroy({ where: { id: req.params.account_id } }).then(function(){ res.redirect('/'); }); }); ``` ### 優化 新增一個單獨顯示某筆消費的頁面 ``name/routers/accounts.js`` 記得要放在最下面,不然會和上面的 ``router.get('/create'...)`` 有衝突 ```javascript= /* 某筆資料單獨頁面 */ router.get('/:account_id/', function(req, res, next) { models.Account.findOne({ where: { id: req.params.account_id } }).then(function(account){ res.render('single_account', {account: account}); }); }); ``` 接著撰寫顯示的頁面,並加上麵包屑 ``name/view/single_account.ejs`` ```javascript= <!DOCTYPE html> <html> <head> <title>記帳囉</title> <link rel='stylesheet' href='/stylesheets/style.css' /> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous"> </head> <body> <nav class="navbar navbar-expand-md navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" href="/">記帳囉</a> <div class="ml-auto my-2 my-lg-0"> <a href="/accounts/create"> <button class="btn btn-outline-success my-2 my-sm-0">新增消費</button> </a> </div> </div> </nav> <div class="container mt-3"> <!-- 麵包屑 --> <nav aria-label="breadcrumb" role="navigation"> <ol class="breadcrumb"> <li class="breadcrumb-item"> <a href="/">Home</a> </li> <li class="breadcrumb-item active" aria-current="page"><%= account.title %></li> </ol> </nav> <div class="row"> <div class="col-12"> <table class="table table-hover"> <tr> <th>消費內容</th> <th>類型</th> <th>金額</th> </tr> <tr> <td> <%= account.title %> </td> <td> <%= account.type %> </td> <td> <%= account.cost %> </td> </tr> </table> </div> </div> </div> <footer class="container mt-5"> <hr> <p>&copy; Company 2017</p> </footer> </body> </html> ``` ### 成品 ![](https://i.imgur.com/t2v92Px.png) ![](https://i.imgur.com/bJM1Sgj.png) ![](https://i.imgur.com/FtQSNEQ.png) ### 登入功能 可以使用 [passport 套件](http://www.passportjs.org/)