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信息)
计算有效时间的规则如下:
- 如果配置给
redis
的ttl
存在的话,过期时间为cookie设置的时间。
2.如果cookie的过期时间为数字的话,则过期时间为cookie设置的时间。 - 如果上面两条规则不符合的话,则设置为默认有效时间(一天)
(我有个疑问,上面代码的||
应该是&&
才对吧?)
整体流程
// 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);
});
}
小结
- 这个实现相当简单,封装了几个redis的方法(get, set, expire)
- 虽然比较简单,但是可配置性还是挺好的,上面的options几乎囊括了要配置的信息。比如,是否允许过期等
- 这里有个小技巧,如果传入的方法为空,而你又需要这个方法但是允许为空(不报错),我们可以设置这个方法为空方法(说白了就是一个默认方法而已,哈哈)
- 将计算过期时间的方法抽象出来,这个还是不错的。一个方法一个功能,而且有利于重用
- 我有一个小小的疑惑:为什么有时候是
fn.apply(this, arguments)
, 而有时又是fn.apply(null, argments)
呢?难道是有些回调方法需要读取当期redis实例的某些属性或者方法?
最后
由于这只是一个session的插件,所以单独拿出来讲,意义不大。下次我讲express-session
看完之后也写出来,这样才会更有意思(内存存储比redis存储的代码有趣多了。真的,相信我)