終於來到了教學的尾聲,回顧我們從一開始從中間件直接回傳hello koa,緊接著我們加上了router以及logger,讓我們擁有管理URL的能力,再到使用模板引擎,動態渲染出網頁,上一章我們學會如何處理用戶傳送過來的表單。而我們這個章節將要介紹最後一個重要的環節,也就是如何將用戶瀏覽的狀態給儲存下來。

Http是一種無連接,無狀態性的協定。Server沒有任何記憶能力,因此他不知道這個Client是否曾經連線過,Server只在乎Client這個請求需要些什麼,我盡力傳送你想要的資訊給你,當我把資料回傳後,Server與Client就會立即斷線。但是使用者總是希望Server端能夠記住當下的狀態,例如記住登入狀態,或者是記住我購物車中的內容等等,因此我們使用Cookie以及Session這兩項工具來達成我們的目的,當我們發送Http request時,我們會將Cookie傳送回去Server端,Server端藉由判斷這個Cookie來決定使用者當前的狀態。

Cookie與Session可以想像成是整個網站對於這個連線者的全域變數,因此在每一個router下都是共享的

Cookie是key-value的pair,在Koa中我們可以使用ctx.set(KEY, VALUE)來設定Cookie,使用ctx.get(KEY)來讀取Cookie,如果我們想要清除Cookie則必須使用ctx.set(KEY, undefined)

我們延續上次Youtube單曲循環的範例,只是我們加上個小功能,我們會記住最後一次播放的影片,當使用者瀏覽/loop時我們自動播放最後一次的影片。

    
// cookie.ts
import Koa = require('koa');
import Router = require('koa-router');
import Path = require('path');
import Views = require('koa-views');

const app = new Koa();
const router = new Router();

app.use(Views(Path.join(__dirname, 'views'), {
    extension: 'pug',
    map: {
        pug: 'pug',
    },
}));

router.get('/loop', async (ctx) => {
    const VideoID = ctx.query.VideoID ? ctx.query.VideoID : ctx.cookies.get('VideoID'); // 當我們沒有傳遞參數時,嘗試去讀取cookie的內容,播放上一次的內容
    ctx.cookies.set('VideoID', VideoID);    // 將本次的VideoID存到cookie中
    await ctx.render('looper', {
        VideoID: VideoID ? VideoID : undefined,
    });
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000);

我們首先在18行的部分嘗試讀取Cookie的內容,在第19行時將此次的影片ID給存入Cookie中,下次當我們再次瀏覽這頁時,我們就可以透過第18行的ctx.cookies.get('VideoID')取得最後一次播放的影片ID。

特別注意的是,Cookie是明文存放在瀏覽器中,因此我們不能再Cookie中存放敏感的訊息。 Image

Session

因為Cookie的明文性,因此我們會使用另外一個替代方案,這個方案就是Session,Session依舊會使用到Cookie,但是Cookie中只會存放一個SessionID,Server收到這個SessionID後再去找出這組SessionID對應到的內容,因此前端用戶不知道Session實際上代表的意義,達到安全性上的需求,但也因為Server要額外花空間去記憶Session的資料,因此使用Session會比Cookie更佔用資源,如果訊息不敏感,使用Cookie會是一個好選擇。

網站的登入、登出功能就是使用Session來達成

要在Koa使用Session,我們必須安裝koa-session中間件,

    
yarn add koa-session @types/koa-session

使用上僅需要app.use(Session(app))將Session中間件串接上去即可開始使用,Session中間件會在ctx增加ctx.session物件,我們透過存取ctx.session物件就可以操作session。

延續剛剛的範例,我們再加如一個小功能,使用Session記錄曾經請求過哪些影片

    
// session.ts
import Koa = require('koa');
import Router = require('koa-router');
import Path = require('path');
import Views = require('koa-views');
import Session = require('koa-session');

const app = new Koa();
const router = new Router();

app.keys = ['MySecretKey']

app.use(Views(Path.join(__dirname, 'views'), {
    extension: 'pug',
    map: {
        pug: 'pug',
    },
}));

app.use(Session(app));

router.get('/loop', async (ctx) => {
    const VideoID = ctx.query.VideoID ? ctx.query.VideoID : ctx.cookies.get('VideoID');
    ctx.cookies.set('VideoID', VideoID);
    if(ctx.session) {                                                   // 判斷ctx.session是否存在
        if(VideoID) {
            if(ctx.session.orderedVideos){
                if(ctx.session.orderedVideos.indexOf(VideoID)==-1){
                    ctx.session.orderedVideos.push(VideoID);            // 將請求過的影片放入ctx.session.orderedVideos中
                }
            }
            else {
                ctx.session.orderedVideos = [];                         // 如果ctx.session.orderedVideos不存在,則初始化他為一個空陣列
            }
        }
    }
    await ctx.render('looper', {
        VideoID: VideoID ? VideoID : null,
        orderedVideos: ctx.session ? ctx.session.orderedVideos : null,
    });
});

router.get('/reset', async (ctx) => {
    ctx.cookies.set('VideoID', undefined);                              // 清除Cookie
    ctx.session = null;                                                 // 清除Session
    ctx.redirect('/loop');                                              // 重導向回/loop頁面
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000);

References:
1. koajs/session
2. koa
所有範例中的程式碼皆放置在koa-tutorial-sample專案中