connect-redis

github 传送门

简介

这是一个关于session的持久化插件, 配合 express-session使用。此模块基于redis,将session相关信息持久化。根据express的文档,我们只要实现其所要求的部分方法即可。

需要实现的方法

必选
store.destroy(sid, callback)
store.get(sid, callback)
store.set(sid, session, callback)
可选
store.clear(callback)
store.length(callback)
推荐
store.touch(sid, session, callback)

原则上只要实现必选方法即可,但是最好还是加上推荐的方法吧。

源码分析

引入模块
var debug = require('debug')('connect:redis');
var redis = require('redis');
var util = require('util');
var noop = function(){};

由代码可知,该插件基于``redis, 当然我们也可以用ioredis`代替。

获取session的生命周期
var oneDay = 86400;
function getTTL(store, sess) {
  var maxAge = sess.cookie.maxAge;
  return store.ttl || (typeof maxAge === 'number'
    ? Math.floor(maxAge / 1000)
    : oneDay);
}

store:存放session信息的类(这里是redis实例)
sess :session信息(内存的session信息)
计算有效时间的规则如下:

  1. 如果配置给redisttl存在的话,过期时间为cookie设置的时间。
    2.如果cookie的过期时间为数字的话,则过期时间为cookie设置的时间。
  2. 如果上面两条规则不符合的话,则设置为默认有效时间(一天)
    (我有个疑问,上面代码的 || 应该是 && 才对吧?)
整体流程
// session: express-session类
module.exports = function (session) {

  // 这个session.Store是保存session信息的类
  var Store = session.Store;

  // redis实例,继承session.Store。options是一系列配置的值,下文会详细介绍
  function RedisStore (options) { }
  util.inherits(RedisStore, Store);

  // 根据id获取session
  RedisStore.prototype.get = function (sid, fn) { };

  // 根据id和session写入redis
  RedisStore.prototype.set = function (sid, sess, fn) { };

  // 删除当前session信息
  RedisStore.prototype.destroy = function (sid, fn) { };
  
  // 更新当前session的有效时间
  RedisStore.prototype.touch = function (sid, sess, fn) { };

  return RedisStore;
};

可以看到,这里只实现了必选方法和推荐方法。只要调用了这个方法,就会返回一个封装过的redis类。
其中,options的选项如下:

1. ttl: 过期时间,默认是session.maxAge, 或者是一天
2. disableTTL: 是否允许redis的key有过期时间。这个值优先于ttl
3. db: redis哪个数据库,默认是0
4. pass: 密码
5. prefix: key的前缀,默认是 'sess:'
6. unref: 这个方法作用于底层socket连接,可以在程序没有其他任务后自动退出。
7. serializer: 包含stringify和parse的方法,用于格式化存入redis的值。默认是JSON
8. logErrors: 是否打印redis出错信息,默认false
   如果值为true,则会提供一个默认的处理方法(console.error);
   如果是一个函数,则redis的报错信息由它来处理
   如果值为false,则不处理出错信息
RedisStore类
  function RedisStore (options) {
    if (!(this instanceof RedisStore)) {
      throw new TypeError('Cannot call RedisStore constructor as a function');
    }

    var self = this;

    options = options || {};
    Store.call(this, options);   // 初始化父类
    this.prefix = options.prefix == null
      ? 'sess:'
      : options.prefix;

    delete options.prefix;

    this.serializer = options.serializer || JSON;

    if (options.url) {
      options.socket = options.url;  // redis地址
    }

    // convert to redis connect params
    if (options.client) {
      this.client = options.client;
    }
    else if (options.socket) {
      this.client = redis.createClient(options.socket, options);
    }
    else {
      this.client = redis.createClient(options);  // 默认本机无密码的redis
    }
    
    // logErrors
    if(options.logErrors){
      // if options.logErrors is function, allow it to override. else provide default logger. useful for large scale deployment
      // which may need to write to a distributed log
      if(typeof options.logErrors != 'function'){
        options.logErrors = function (err) {
          console.error('Warning: connect-redis reported a client error: ' + err);
        };
      }
      this.client.on('error', options.logErrors); 
    }

    if (options.pass) {   // 需要密码
      this.client.auth(options.pass, function (err) {
        if (err) {
          throw err;
        }
      });
    }

    this.ttl = options.ttl;
    this.disableTTL = options.disableTTL;

    if (options.unref) this.client.unref();

    if ('db' in options) {
      if (typeof options.db !== 'number') {
        console.error('Warning: connect-redis expects a number for the "db" option');
      }

      self.client.select(options.db);   // 连接所配置的数据库
      self.client.on('connect', function () {
        self.client.select(options.db);
      });
    }

    self.client.on('error', function (er) {
      debug('Redis returned err', er);
      self.emit('disconnect', er);   // 由于父类继承EventEmitter,所以有事件功能
    });

    self.client.on('connect', function () {
      self.emit('connect');    // 同理
    });
  }
获取当前session
RedisStore.prototype.get = function (sid, fn) {
    var store = this;
    var psid = store.prefix + sid;
    if (!fn) fn = noop;
    debug('GET "%s"', sid);

    store.client.get(psid, function (er, data) {
      if (er) return fn(er);   // 报错
      if (!data) return fn();  // 可能失效了

      var result;
      data = data.toString();
      debug('GOT %s', data);

      try {
        result = store.serializer.parse(data);   //  转化为object
      }
      catch (er) {
        return fn(er);
      }
      return fn(null, result);   // 返回结果
    });
  }
设置当前session
RedisStore.prototype.set = function (sid, sess, fn) {
    var store = this;
    var args = [store.prefix + sid];
    if (!fn) fn = noop;

    try {
      var jsess = store.serializer.stringify(sess);
    }
    catch (er) {
      return fn(er);
    }

    args.push(jsess);

    if (!store.disableTTL) {   // 需要设置有效时间
      var ttl = getTTL(store, sess);
      args.push('EX', ttl);
      debug('SET "%s" %s ttl:%s', sid, jsess, ttl);
    } else {
      debug('SET "%s" %s', sid, jsess);
    }

    store.client.set(args, function (er) {
      if (er) return fn(er);    // 报错
      debug('SET complete');
      fn.apply(null, arguments);
    });
  }
销毁当前session
RedisStore.prototype.destroy = function (sid, fn) {
    sid = this.prefix + sid;
    debug('DEL "%s"', sid);
    this.client.del(sid, fn);
  }

简单粗暴,没啥好说的

更新当前session有效时间
RedisStore.prototype.touch = function (sid, sess, fn) {
    var store = this;
    var psid = store.prefix + sid;
    if (!fn) fn = noop;
    if (store.disableTTL) return fn();  // 不能设置有效期(一直有效)

    var ttl = getTTL(store, sess);

    debug('EXPIRE "%s" ttl:%s', sid, ttl);
    store.client.expire(psid, ttl, function (er) {
      if (er) return fn(er);
      debug('EXPIRE complete');
      fn.apply(this, arguments);
    });
  }

小结

  1. 这个实现相当简单,封装了几个redis的方法(get, set, expire)
  2. 虽然比较简单,但是可配置性还是挺好的,上面的options几乎囊括了要配置的信息。比如,是否允许过期等
  3. 这里有个小技巧,如果传入的方法为空,而你又需要这个方法但是允许为空(不报错),我们可以设置这个方法为空方法(说白了就是一个默认方法而已,哈哈)
  4. 将计算过期时间的方法抽象出来,这个还是不错的。一个方法一个功能,而且有利于重用
  5. 我有一个小小的疑惑:为什么有时候是fn.apply(this, arguments), 而有时又是 fn.apply(null, argments)呢?难道是有些回调方法需要读取当期redis实例的某些属性或者方法?

最后

由于这只是一个session的插件,所以单独拿出来讲,意义不大。下次我讲express-session看完之后也写出来,这样才会更有意思(内存存储比redis存储的代码有趣多了。真的,相信我)

你可能感兴趣的:(connect-redis)