通过koa-session官方example分析源码

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();
    }
};

目前我们应该已经知道了以下几件事

  1. koa-session就是一个中间件
  2. koa-session为context扩展了一个与session相关的属性,在koa-session之后插入的逻辑都可以访问并操作这个属性。
  3. 待后续逻辑都执行结束后,koa-session保存session

仍然困惑的问题

  1. context扩展了一个什么样的属性
  2. 这个属性得值是从何处来,又是如何存储的

koa-session源码目录结构

image

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的基本属性,这里不再详细叙述。

image

好了,到现在我们已经清楚了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方法,用什么方式存储都是可以的)。

至此,全文结束

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值