koa-session 通过官方example分析源码
1. 如何使用koa-session
- 启动文件引入koa-session
var session = require('koa-session');
var Koa = require('koa');
var app = new Koa();
app.keys = ['koa session test'];
app.use(session(app));
- 存取session
app.use(async (ctx, next) => {
if ('/favicon.ico' == ctx.path) return;
var n = ctx.session.views || 0;
ctx.session.views = ++n;
ctx.body = n + ' views';
});
- 访问页面时,就可以看到每访问一次,计数就会加一
2. koa-session是如何做到的
app.use(session(app))
就从这个session函数入手,看看session函数做了什么。
module.exports = function(opts, app) {
// session(app[, opts])
if (opts && typeof opts.use === 'function') {
[ app, opts ] = [ opts, app ];
}
// app required
if (!app || typeof app.use !== 'function') {
throw new TypeError('app instance required: `session(opts, app)`');
}
opts = formatOpts(opts);
extendContext(app.context, opts);
return async function session(ctx, next) {
const sess = ctx[CONTEXT_SESSION];
if (sess.store) await sess.initFromExternal();
try {
await next();
} catch (err) {
throw err;
} finally {
await sess.commit();
}
};
};
这是koa-session的主函数,简单的把这个函数分成三个段落
- 第一段,显然和session没啥关系,不解释,看下一段
// session(app[, opts])
if (opts && typeof opts.use === 'function') {
[ app, opts ] = [ opts, app ];
}
// app required
if (!app || typeof app.use !== 'function') {
throw new TypeError('app instance required: `session(opts, app)`');
}
- 第二段,执行了两个函数,对传入的参数做了某种操作,根据这两个函数的名称可以推断:formatOpts是格式化配置项;extendContext则是扩展了“app”的“context”,记住这个extendContext,秘密都在这里。
opts = formatOpts(opts);
extendContext(app.context, opts);
- 第三段,首先从context中拿到了session对象,并等待所有后续中间件执行结束后,执行了一下这个对象的“commit”方法,根据名称猜测,应该是做了一个保存操作。至此,koa-session的主流程就结束了。
return async function session(ctx, next) {
const sess = ctx[CONTEXT_SESSION];
if (sess.store) await sess.initFromExternal();
try {
await next();
} catch (err) {
throw err;
} finally {
await sess.commit();
}
};
目前我们应该已经知道了以下几件事
- koa-session就是一个中间件
- koa-session为context扩展了一个与session相关的属性,在koa-session之后插入的逻辑都可以访问并操作这个属性。
- 待后续逻辑都执行结束后,koa-session保存session
仍然困惑的问题
- context扩展了一个什么样的属性
- 这个属性得值是从何处来,又是如何存储的
koa-session源码目录结构
koa-session源码包含3个主要文件
- index
- context
- session
extendContext
现在我们回过头来看extendContext函数,看看koa-session究竟为context扩展了一个什么样的属性。
在看extendContext的实现代码之前,先来回忆一下最开始的sample中我们如何读写的session
let n = ctx.session.viewCount || 0
ctx.session.viewCount = ++n;
根据这段代码,可以确定刚才所说的扩展出的属性名为“session”,我们从“session”中读取并修改了一个叫做“viewCount”的值。
function extendContext(context, opts) {
Object.defineProperties(context, {
[CONTEXT_SESSION]: {
get() {
if (this[_CONTEXT_SESSION]) return this[_CONTEXT_SESSION];
//文件头 const ContextSession = require('./lib/context');
this[_CONTEXT_SESSION] = new ContextSession(this, opts);
return this[_CONTEXT_SESSION];
},
},
session: {
get() {
return this[CONTEXT_SESSION].get();
},
set(val) {
this[CONTEXT_SESSION].set(val);
},
configurable: true,
},
sessionOptions: {
get() {
return this[CONTEXT_SESSION].opts;
},
},
});
}
综合这两段代码,可以看出,当执行ctx.session时,实际上执行的是“ContextSession”对象(lib/context.js)的get方法
context.js部分代码
get() { //....... this.initFromCookie(); return this.session; } initFromCookie() { //...... this.create(); //...... } create(val, externalKey) { //...... this.session = new Session(this.ctx, val); }
关注这两行
return this.session;
this.session = new Session(this.ctx, val);
现在看到,我们使用的ctx.session.viewCount中的session就是“lib/session”。“lib/session”作为基本数据模型,提供了session的基本属性,这里不再详细叙述。
好了,到现在我们已经清楚了koa-session中的主要数据结构,接下来我们看看用户的数据是如何保存进session的。
创建session对象
前面介绍了,在“ContextSession”的getter通过调用initFromCookie创建了session对象,但是在调用initFromCookie时却做了这样一个判断
if (!this.store) this.initFromCookie();
而在实现中间件的代码中
if (sess.store) await sess.initFromExternal();
实际上,koa-session提供了两种session保存方式,默认为cookie,也可以通过初始化时的配置项,使用其他方式进行存储,如redis或者其他DB。
“ContextSession”为这两种存储方式提供了两个不同的创建session的入口,分别是:
- initFromCookie
- initFromExternal
initFromCookie() {
//......
const cookie = ctx.cookies.get(opts.key, opts);
if (!cookie) {
this.create();
return;
}
//......
let json;
json = opts.decode(cookie);
this.create(json);
this.prevHash = util.hash(this.session.toJSON());
}
async initFromExternal() {
//......
const externalKey = ctx.cookies.get(opts.key, opts);
//...如果externalKey不存在就直接调用create方法生成一个...
const json = await this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling });
this.create(json, externalKey);
this.prevHash = util.hash(this.session.toJSON());
}
一句话描述这两个函数,就是从存储介质(cookie或其他数据源)中拿到数据,然后执行create方法创建一个session对象。
ok,现在我们解决下面这个疑惑
数据是怎么存进去的——commit
还记得koa-session中间件中最后那个commit吗?接下来我们就看看这个commit干了什么。
async commit() {
//.......
// removed
if (session === false) {
await this.remove();
return;
}
const reason = this._shouldSaveSession();
debug('should save session: %s', reason);
if (!reason) return;
if (typeof opts.beforeSave === 'function') {
debug('before save');
opts.beforeSave(ctx, session);
}
const changed = reason === 'changed';
await this.save(changed);
}
commit负责做两件事:
- 当设置session为false时删除session
- session发生变化时(_shouldSaveSession返回值不为空时),保存session
koa-session在执行save之前还给留了一个hook——beforeSave,只不过这个hook是没法阻止保存行为的,也许koa认为就不应该阻止,好吧,我想多了。
删除不用说了,怎么保存的呢?来看一下save方法
async save(changed) {
//......
// save to external store
if (externalKey) {
if (typeof maxAge === 'number') {
// ensure store expired after cookie
maxAge += 10000;
}
await this.store.set(externalKey, json, maxAge, {
changed,
rolling: opts.rolling,
});
this.ctx.cookies.set(key, externalKey, opts);
return;
}
// save to cookie
json = opts.encode(json);
this.ctx.cookies.set(key, json, opts);
}
save方法负责将变化的session存入指定的介质(在这可以看到,如果不适用默认的cookie,其实这个store并非一定是数据库,只要提供get和set方法,用什么方式存储都是可以的)。
至此,全文结束