我們公司的大佬,搭建了一套基於koa2的node框架,雖然說是重複造輪子,但適用當前場景的輪子才是最好的,何況很多人還造不出輪子呢~ 大佬搭建的框架命名為 Sponse
,其中有很多出色的設計,這裡對 session
的設計做個總結(其實是為了自己加深印象,學習大佬的設計)
Sponse意思是海綿,而我們這套框架就如同海綿一樣,通過不斷吸收其他框架的優秀設計豐滿自己
cookie 與 session
做開發的小夥伴對這兩個應該在熟悉不過了,這兩個一起構建起前後端狀態的聯絡,常見的如維護使用者登入狀態,使用者登入後,需要在服務端記錄下該使用者的登入狀態,前端才可以使用需要登入態的介面,此時,瀏覽器中的 cookie
就是查詢使用者是否登入的憑證,在服務端,通常是將使用者狀態資訊儲存在快取中,簡單說,就是基於 cookie的session
呼叫方式
優秀的架構中,呼叫方式必須是友好的
期望能夠通過上下文直接呼叫,如:獲取session物件 ctx.session
; 設定session的值 ctx.session.name = name
邏輯圖
整體實現邏輯如下
實現
- koa提供了操作cookie的api,開箱即用,官方截圖如下
- 快取基於redis實現
詳細程式碼如下:
Session 物件
首先構建一個session物件
核心方法:
save
用於同步cookiechanged
用於檢測session物件是否發生修改,為了同步更新快取中的值
export class Session {
private _ctx;
isNew: boolean;
_json; // session物件的json串,用於比較session物件是否發生變化
constructor(ctx, obj) {
this._ctx = ctx;
if (!obj) this.isNew = true;
else {
for (const k in obj) {
this[k] = obj[k];
}
}
}
/**
* Save session changes by
* performing a Set-Cookie.
*
* @api private
*/
save() {
const ctx = this._ctx;
const json = this._json || JSON.stringify(this.inspect());
const sid = ctx.sessionId;
const opts = ctx.cookieOption;
const key = ctx.sessionKey;
if (ctx.cookies.get(key) !== sid) {
// 設定cookies的值
ctx.cookies.set(key, sid, opts);
}
return json;
};
/**
* JSON representation of the session.
*
* @return {Object}
* @api public
*/
inspect() {
const self = this;
const obj = {};
Object.keys(this).forEach(function (key) {
if ('isNew' === key) return;
if ('_' === key[0]) return;
obj[key] = self[key];
});
return obj;
}
/**
* Check if the session has changed relative to the `prev`
* JSON value from the request.
*
* @param {String} [prev]
* @return {Boolean}
* @api private
*/
changed(prev) {
if (!prev) return true;
this._json = JSON.stringify(this.inspect());
return this._json !== prev;
};
}
複製程式碼
SessionEngine koa中介軟體
koa 當然是離不開中介軟體了,洋蔥模型酷炫到不行,非常方便的解決了session物件與快取的同步
decorator(app) {
app.keys = ['signed-key'];
const CONFIG = {
key: app.config.name + '.sess', /** (string) cookie key (default is koa:sess) */
cookie: {
// maxAge: 1000, /** (number) maxAge in ms (default is 1 days) */
overwrite: false, /** (boolean) can overwrite or not (default true) */
httpOnly: true, /** (boolean) httpOnly or not (default true) */
signed: true, /** (boolean) signed or not (default true) */
}
};
app.use(async (ctx, next) => {
let sess: Session;
let sid;
let json;
ctx.cookieOption = CONFIG.cookie;
ctx.sessionKey = CONFIG.key;
ctx.sessionId = null;
// 獲取cookie 對應的值, 即sessionID,就是快取中的key
sid = ctx.cookies.get(CONFIG.key, ctx.cookieOption);
// 獲取session值
if (sid) {
try {
// 若key存在,則從快取中獲取對應的值
json = await app.redisClient.get(sid);
} catch (e) {
console.log('從快取中讀取session失敗: %s\n', e);
json = null;
}
}
// 例項化session
if (json) {
// 若快取中有值,則基於快取中的值構建session物件
ctx.sessionId = sid;
try {
sess = new Session(ctx, json);
} catch (err) {
if (!(err instanceof SyntaxError)) throw err;
sess = new Session(ctx, null);
}
} else {
sid = ctx.sessionId = sid || Uuid.gen();
sess = new Session(ctx, null);
}
// 為了便於使用,將session掛載到上下文,這樣就可以 ctx.session 這麼使用了
Object.defineProperty(ctx, 'session', {
get: function () {
return sess;
},
set: function (val) {
if (null === val) return sess = null;
if ('object' === typeof val) return sess = new Session(this, val);
throw new Error('this.session can only be set as null or an object.');
}
});
try {
await next();
} catch (err) {
throw err;
} finally {
if (null === sess) {
// 設定session=null表示清空session
ctx.cookies.set(CONFIG.key, '', ctx.cookieOption);
} else if (sess.changed(json)) {
// 檢查 session 是否發生變化,若有變化,更新快取中的值
json = sess.save();
await app.redisClient.set(sid, json);
app.redisClient.ttl(sid);
// 設定redis值過期時間為60分鐘
app.redisClient.expire(sid, 7200);
} else {
// session 續期
app.redisClient.expire(sid, 7200);
}
}
});
複製程式碼
小結
多多學習~ 點點進步~