Node.js自學筆記 (7/12)

Session

  1. Session是架在Cookie的上層,Session的id是存在Cookie裡面
  2. 若 Client 的瀏覽器停在某個網頁,使用者可能某些原因久久未再拜訪該網站,或者根
    本就已離開該站。此時會依 Session 的存活時間,決定 Session 是否有效。
  3. Server 是以 Client 最後一次拜訪開始重新計時的,若 Client 在 Session 存活時間內,
    持續訪問該站,Session 就會一直有效。
  4. 利用 Cookie 存放「Session ID」,在 Client 第一次拜訪時將 Session ID 存入 Cookie。
  5. 有了 Session ID 之後,Server 會在主機(記憶體、檔案或資料庫)為每個 Session ID
    建立一個對應的 Session 物件,資料就存在 Session 物件裡。

安裝 express-session:

npm i express-session

範例:顯示頁面刷新次數

const session = require('express-session');

// session()是一個function,呼叫它去建立middleware
app.use(session({
    // 新用戶沒有使用到 session 物件時不會建立 session 和發送 cookie
    // saveUninitialized與resave是官方建議要設定的,若沒有設定,會有warning
    saveUninitialized: false,
    resave: false, // 沒變更內容是否強制回存
    secret: 'kkk123',  // 加密用的字串
    // 若把下面的cookie設定拿掉,就等於沒有設定session的存活時間,待瀏覽器關閉才會消失
    cookie: {
        maxAge: 1200000, // session的存活時間,20分鐘,單位毫秒
    }
}));

// top-level middleware設定好session之後,就可以在top-level middleware使用session
app.get('/try-session', (req, res) => {
    req.session.my_var = req.session.my_var || 0; // 預設為 0
    req.session.my_var++;
    res.json({
        my_var: req.session.my_var,
        session: req.session
    });
});

執行結果:

測試:
如果把index.js隨便新增一行空白,然後儲存,就會觸發nodemon重新啟動node.js server。這時再連到 http://localhost:3000//try-session ,就會發現計數器從0開始計算。

打開chrome的Network,可以觀察client跟server之間的資料傳遞。

Response 裡面的 Set-Cookie 是server傳回來的檔頭,要求使用者的瀏覽器設定Cookie,其中**connect.sid**是Express-Session的Session ID。另外也有路徑(Path),過期時間(Expires)。

Request 的時候,同樣也是把Cookie的資訊從client端送回server端(如下圖),請參考下圖的Request Header的Cookie。

製作登入表單

先做 樣板 ,到 /views 資料夾,新增 /view/login.ejs ,檔案內容如下:

<%- include ('parts/html-head') %>
<%- include ('parts/navbar') %>
<div class="container">
    <form name="form1" method="post" onsubmit="return formCheck();"> <!-- onsubmit為事件處理器 -->
        <div class="form-group">
            <label for="account">Account</label>
            <input type="email" class="form-control" id="account" name="account">
        </div>
        <div class="form-group">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" name="password">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
</div>
<%- include('parts/scripts') %>
<script>
    function formCheck(){
        return false; // 目的為不讓表單送出
    }
</script>
<%- include('parts/html-foot') %>

index.js 新增以下路由:

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

app.post('/login', (req, res)=>{
    // res.render('login');
    const output={

    };
});

就可以在 http://localhost:3000/login 看到以下登入畫面:


隨便填資料按下送出,並不會真的送出,因為:onsubmit="retuen formCheck()" 中,formCheck() 回傳 false,所以不會將表單送出。

原則上應該要在 formCheck() 設定檢查的功能,如,檢查欄位有沒有值,判斷格式是否正確,如果確認正確再送AJAX的送出資料。

我們接下來要用fetch實作AJAX,關於fetch可以參考:Using Fetch - Web APIs | MDN
login.ejs<script></script> 改為:

<script>
    function formCheck() {
        const fd = new FormData(document.form1);
        fetch('/login', {
            method: 'POST',
            body: fd
        })
            .then(r => r.json()) // 後端傳JSON到前端
            .then(obj => {
                console.log(obj)
            });
        return false; // 目的為不讓表單送出
    };
</script>

實際在 http://localhost:3000/login 填寫表單然後送出資料,會發現body沒有收到任何資訊:

這是因為前端送到後端的資料是 multipart/form-dataexpress.js 的中介軟體 expressjs/body-parser 無法解析,所以我們要用一個 middleware ,要做 multer

解決方法為將

app.post('/login', (req, res)=>{
}

改為

app.post('/login', upload.none(), (req, res)=>{
}

使用 upload 來解析,但是實際沒有上傳任何檔案,所以後面加上 .none

再試一次,測試結果如下,可以真正取到資料了。

記得,使用FormData的時候,要使用expressjs/multer的upload去處理。因為expressjs/body-parser不會處理multipart/form-data資料。

如果一定要用expressjs/body-parser,就不要用FormData,可以改用物件然後做JSON.stringify(),然後記得下檔頭

使用者帳號密碼查驗

因為還沒有設定資料庫連線,所以我們先把特定帳號密碼寫死在程式裡面。

index.js

app.post('/login', upload.none(), (req, res) => {
    // res.render('login');
    const users = {
        '[email protected]': { // [email protected]是key值
            password: '123',
            nickname: 'Benjamin'
        },
        '[email protected]': {
            password: '456',
            nickname: 'Benjamin2'
        }
    };
    const output = {
        success: false,
        body: req.body
    };
    // 檢查帳密是否正確
    if (users[req.body.account] && users[req.body.account].password===req.body.password){
        // 若帳密正確,就把使用者資訊寫入session
        output.success = true;
        req.session.user={
            id: req.body.account,
            nickname: users[req.body.account].nickname
        }
    }
    output.sess_user = req.session.user
    res.json(output);
});

login.ejs

<%- include ('parts/html-head') %>
<%- include ('parts/navbar') %>
<div class="container">
    <form name="form1" method="post" onsubmit="return formCheck();">
        <!-- onsubmit為事件處理器 -->
        <div class="form-group">
            <label for="account">Account</label>
            <input type="email" class="form-control" id="account" name="account">
        </div>
        <div class="form-group">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" name="password">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
</div>
<%- include('parts/scripts') %>
<script>
    function formCheck() {
        const fd = new FormData(document.form1);
        fetch('/login', {
            method: 'POST',
            body: fd
        })
            .then(r => r.json()) // 後端傳JSON到前端
            .then(obj => {
                console.log(obj)
            });
        return false; // 目的為不讓表單送出
    };
</script>
<%- include('parts/html-foot') %>

測試登入,登入成功:

顯示登入狀態

目前我們已使用fetch做AJAX實作搭配session做使用者登入的功能。由於使用AJAX,所以登入後,頁面沒有跳轉,目前在網頁上面也看不到登入是否成功。所以接下來我們要在網頁上面加上登入狀態的顯示。

可以到Bootstrap官網挑選合適的元件(component)來使用。

<div class="alert alert-primary" role="alert">
  A simple primary alert—check it out!
</div>

login.ejs改為:

<%- include ('parts/html-head') %>
<%- include ('parts/navbar') %>
<div class="container">
    <div id="infobar" class="alert" role="alert" style="display: none;">
    </div>
    <form name="form1" method="post" onsubmit="return formCheck();">
        <!-- onsubmit為事件處理器 -->
        <div class="form-group">
            <label for="account">Account</label>
            <input type="email" class="form-control" id="account" name="account">
        </div>
        <div class="form-group">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" name="password">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
</div>
<%- include('parts/scripts') %>
<script>
    const infobar = $('#infobar');

    function formCheck() {
        infobar.hide();
        const fd = new FormData(document.form1);
        fetch('/login', {
            method: 'POST',
            body: fd
        })
            .then(r => r.json()) // 後端傳JSON到前端
            .then(obj => {
                console.log(obj)
                if (obj.success) {
                    infobar
                        .removeClass('alert-danger')
                        .removeClass('alert-success')
                        .addClass('alert-success')
                        .text('登入成功');
                    setTimeout(() => {
                        location.reload();
                    }, 1000);
                } else {
                    infobar
                        .removeClass('alert-danger')
                        .removeClass('alert-success')
                        .addClass('alert-danger')
                        .text('登入失敗,帳號或密碼錯誤');
                }
            });
        infobar.show();
        return false; // 目的為不讓表單送出
    };
</script>
<%- include('parts/html-foot') %>

實測結果如下:
2020-11-09_12-58-37

新增訊息自動收藏功能

login.ejs 的部分程式改為:

                if (obj.success) {
                    infobar
                        .removeClass('alert-danger')
                        .removeClass('alert-success')
                        .addClass('alert-success')
                        .text('登入成功');
                    setTimeout(() => {
                        location.reload();
                    }, 1000);
                } else {
                    infobar
                        .removeClass('alert-danger')
                        .removeClass('alert-success')
                        .addClass('alert-danger')
                        .text('登入失敗,帳號或密碼錯誤');
                    setTimeout(() => {
                        infobar.slideUp();
                    }, 1000);
                }

實測結果如下:
2020-11-09_13-14-52

顯示歡迎訊息

接下來要做,如果登入成功,就把帳密的表單隱藏起來,然後顯示出歡迎訊息。

login.ejs 改為:

<%- include ('parts/html-head') %>
<%- include ('parts/navbar') %>
<div class="container">
    <% if (sess.user) { %>
        <h2><%= sess.user.nickname %>, 你好 </h2>
        <p><a href="/logout">登出</a></p>
    <% } else { %>
    <div id="infobar" class="alert" role="alert" style="display: none;">
    </div>
    <form name="form1" method="post" onsubmit="return formCheck();">
        <!-- onsubmit為事件處理器 -->
        <div class="form-group">
            <label for="account">Account</label>
            <input type="email" class="form-control" id="account" name="account">
        </div>
        <div class="form-group">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" name="password">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    <% } %>
</div>
<%- include('parts/scripts') %>
<script>
    const infobar = $('#infobar');

    function formCheck() {
        infobar.hide();
        const fd = new FormData(document.form1);
        fetch('/login', {
            method: 'POST',
            body: fd
        })
            .then(r => r.json()) // 後端傳JSON到前端
            .then(obj => {
                console.log(obj)
                if (obj.success) {
                    infobar
                        .removeClass('alert-danger')
                        .removeClass('alert-success')
                        .addClass('alert-success')
                        .text('登入成功');
                    setTimeout(() => {
                        location.reload();
                    }, 1000);
                } else {
                    infobar
                        .removeClass('alert-danger')
                        .removeClass('alert-success')
                        .addClass('alert-danger')
                        .text('登入失敗,帳號或密碼錯誤');
                    setTimeout(() => {
                        infobar.slideUp();
                    }, 1000);
                }
            });
        infobar.show();
        return false; // 目的為不讓表單送出
    };
</script>
<%- include('parts/html-foot') %>

index.js 改為:

app.use((req, res, next) => {
    res.locals.sess = req.session || {};
    
    // 要預處理的事情
    // res.locals.userData = {
    //     name: 'benctw',
    //     id: 386,
    //     action: 'edit'
    // }
    next();
});

實測:
2020-11-09_15-10-50

登出

index.js 加入以下程式:

app.get('/logout', (req, res) => {
    delete req.session.user;
    res.redirect('/login');
})

小結 res 的成員:

  1. res.end
  2. res.send
  3. res.render
  4. res.json
  5. res.redirect

將session存入資料庫

瀏覽 express-session專案,在「Compatible Session Stores」一節中,可以看到express-session支援儲存到許多地方,如sqlite, mysql, mongodb, redis等。現在要實作將session存到mysql。

先安裝 express-mysql-session

npm i express-mysql-session

index.js 加入:

const session = require('express-session');
const MysqlStore = require('express-mysql-session')(session);
const db = require(__dirname + '/db_connect2');
const sessionStore = new MysqlStore({}, db);
app.use(session({
    saveUninitialized: false, // 新用戶沒有使用到 session 物件時不會建立 session 和發送 cookie
    resave: false, // 沒變更內容是否強制回存
    secret: 'kkk123',  // 加密用的字串
    store: sessionStore,
    cookie: {
        maxAge: 1200000, // 20分鐘,單位毫秒
    }
}));