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 »</a>
</p>
</div>
</div>
<div class="container">
<div class="row">
</div>
</div>
</main>
<footer class="container">
<hr>
<p>© 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>© 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>© Company 2017</p>
</footer>
</body>
</html>
```
### 成品



### 登入功能
可以使用 [passport 套件](http://www.passportjs.org/)