Aitter's Blog

Cookie.js源码解析

Github上有一个开源的操作Cookie的工具类,非常简洁,代码不到100行,压缩之后不到2kb,这个工具类,最大的特点就是API极其简单,语法糖设计很巧妙,让你分分钟就能记住它的使用方法,这正是封装的作用体现,让使用者不用关心内部实现,简单使用,没有记忆和学习成本。

先来看看Cookie.js的源码部分,随后,我们在用ES6的语法重构,让代码量更加精减。

API介绍

cookie(key, value, num/{})
key cookie的键名
value cookie的值
num 以天为单位的过期时间,1 表示1天后过期,0.1表示1小时后过期

如果第三个参数是一个 json对象,那么它有以下配置

expires: 过期时间,指定cookie的生命期。具体是值是过期日期。如果想让cookie的存在期限超过当前浏览器会话时间,就必须使用这个属性。当过了到期日期时,浏览器就可以删除cookie文件,没有任何影响。 默认浏览器关闭时过期

domain: 域名称,指定关联的WEB服务器或域。值是域名,比如pc175.com。这是对path路径属性的一个延伸。如果我们想让 catalog.pc175.com 能够访问shoppingcart.pc175.com设置的cookies,该怎么办? 我们可以把domain属性设置成“pc175.com”,并把path属性设置成“/”。tag:不能把cookies域属性设置成与设置它的服务器的所在域不同的值。 默认为当前域名

path: 路径,指定与cookie关联的WEB页。值可以是一个目录,或者是一个路径。如果http://www.pc175.com/devhead/index.html 建立了一个cookie,那么在http://www.pc175.com/devhead/目录里的所有页面,以及该目录下面任何子目录里的页面都可以访问这个cookie。这就是说,在http://www.pc175.com/devhead/stories/articles 里的任何页面都可以访问http://www.pc175.com/devhead/index.html建立的cookie。但是,如果http://www.pc175.com/zdnn/ 需要访问http://www.pc175.com/devhead/index.html设置的cookes,该怎么办?这时,我们要把cookies 的path属性设置成“/”。在指定路径的时候,凡是来自同一服务器,URL里有相同路径的所有WEB页面都可以共享cookies。现在看另一个例子:如果想让 http://www.pc175.com/devhead/filters/http://www.pc175.com/devhead/stories/共享cookies,就要把path设成“/devhead”。 默认为当前根目录 /

secure: 是否与服务器交互使用安全传输,指定cookie的值通过网络如何在用户和WEB服务器之间传递。这个属性的值或者是“secure”,或者为空。缺省情况下,该属性为空,也就是使用不安全的HTTP连接传递数据。如果一个 cookie 标记为secure,那么,它与WEB服务器之间就通过HTTPS或者其它安全协议传递数据。不过,设置了secure属性不代表其他人不能看到你机器本地保存的cookie。换句话说,把cookie设置为secure,只保证cookie与WEB服务器之间的数据传输过程加密,而保存在本地的cookie文件并不加密。如果想让本地cookie也加密,得自己加密数据。默认为false

两种用法

// 设置
cookie('test1', '111', 1) or cookie.set('test1', '111', 1)
// 获取
cookie('test1') or cookie.get('test1')
// 删除
cookie('test1', null) or cookie.remove('test1')
// 清除所有cookie
cookie(null) or cookie.clear()
// 获取所有cookie
cookie.all()
// 批量设置cookie
cookie({name1: 'v1', name2: 'v2'}) or cookie.set({name1: 'v1', name2: 'v2'})
// 更多的配置
cookie('test', '121', {
'expires': 7,
'path': '/',
'domain': '',
'secure': true
})

源码结构

基本结构很简单,首先按传统类的定义,定义一个Cookie类的构造函数,用于返回Cookie的实例,且只返回一次,单例模式的应用。

// 定义一个Cookie的类
function Cookie(){
//如果当前类没有实例化,则返回这个Cookie的实例
if(!(this instanceof Cookie)) return new Cookie();
}
// 在类的原型上添加常用操作cookie的方法
Cookie.prototype = {
get:function(){}, //获取cookie
set:function(){}, //设置cookie
remove:function(){}, //删除cookie
clear:function(){}, //清除cookie 调用remove方法,清除一个或多个cookie
all:function(){},//返回所有cookie
}

使用 cookie({test1: 121, test2: 3231, test3: 322}) 向document.cookie中写入cookie信息,以这条记录来对照源码说明:

get

//调用cookie.get('test1')
get: function(name){
//nameEQ值为test1=
var nameEQ = name + "=";
//document.cookie => "test1=121; test2=3231; test3=322"
//把cookie分割成组 ["test1=121", " test2=3231", " test3=322"], 注意第二个和第三个前面是有空字符的
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];//取得字符串
//判断一下字符串有没有前导空格
//这里就是将元素中前面有空字符的去掉
while (c.charAt(0)==' ') {
c = c.substring(1,c.length);//有的话,从第二位开始取
}
//如果含有我们要的name
//判断元素中是否有包含`test1=`,
//如果有的话,判断是否是在字符串的最前面,如果是就使用substring将值截取出来,并调用window.unescape对字符串进行解码,并返回
if (c.indexOf(nameEQ) == 0) {
return unescape(c.substring(nameEQ.length,c.length));//解码并截取我们要值
}
}
return false;
},

注意
这里作者使用了 window.unescape 对字符串解码可能是为了兼容低版本的IE,实际上这个方法已经从Web标准中移除了,虽然现在很多浏览器还支持,但为了尽量符合标准,应该尽量不要使用,改用decodeURIComponent去替代。MDN

set

// set('test4', 12312, 7) or set({ name1:1, name2:2 })
set: function(name, value, options){
//如果第一个参数是obj, 这里处理第二种情况,批量设置cookie
if (isPlainObject(name)) {
for (var k in name) {
if (name.hasOwnProperty(k)) this.set(k, name[k], value);
}
}else{
//如果第三个参数是obj, 则表示是一个自定义的配置
var opt = isPlainObject(options) ? options : { expires: options },
expires = opt.expires !== undefined ? opt.expires : '',
expiresType = typeof(expires),
//默认当前域的是根目录下
path = opt.path !== undefined ? ';path=' + opt.path : ';path=/',
domain = opt.domain ? ';domain=' + opt.domain : '',
secure = opt.secure ? ';secure' : '';
//过期时间
if (expiresType === 'string' && expires !== '') expires = new Date(expires);
//如果num是数值,那么以天的时间为单位
else if (expiresType === 'number') expires = new Date(+new Date + 1000 * 60 * 60 * 24 * expires);
//最后将时间转成GMT的字符串格式
if (expires !== '' && 'toGMTString' in expires) expires = ';expires=' + expires.toGMTString();
//转码后存储到document.cookie
document.cookie = name+"="+escape(value)+expires+path+domain+secure; //转码并赋值
}
},

remove

处理cookie的过期,过期时间设置为-1, 让cookie失效。

remove: function(names){
names = isArray(names) ? names : toArray(arguments);
for (var i = 0, l = names.length; i < l; i++) {
//调用了cookie.set,设置过期时间是-1天,让cookie失效
this.set(names[i], '', -1);
}
return names;
}

clear

//主要用来清除所有cookie,调用时使用 cookie.clear()
//同时也兼容清除单个cookie如:cookie.clear('test1')
clear: function(name){
return name?this.remove(name):this.remove(getKeys(this.all()));
},

all

all:function () {
if (document.cookie === '') return {};
//将cookie分隔成数组
var cookies = document.cookie.split('; '),result = {};
for (var i = 0, l = cookies.length; i < l; i++) {
var item = cookies[i].split('=');
//将键和值解码后返回
result[unescape(item[0])] = unescape(item[1]);
}
//返回结果
return result;
}

下面是重点

语法糖的设计

我们之所以可以使用两种方式去操作cookie,就是作者采用定义一个新的cookie函数,以参数的个数和类型去判断用户是想做什么样的操作,从而调用不同的方法。

然后又将Cookie类的原型上的方法赋值给cookie这个函数,以达到两种操作cookie的方法都能使用,也实现了代码的复用。

这种处理方式,在前面的 ajaxfetch api 的封装中也有应用。

// 提供 cookie('key', 'value', {..}) 的调用方式
var cookie = function(name, value, options){
var args = arguments, cookieIns = Cookie();
// cookie()
if(args.length === 0) return cookieIns.all();
// cookie(null)
if(args.length === 1 && name === null) return cookieIns.clear();
// cookie('token', null)/cookie('token', '')
if(args.length === 2 && !value) return cookieIns.clear(name);
// cookie('token')
if(typeof(name) === 'string' && !value) return cookieIns.get(name);
// cookie('token','1232321423', 1)/cookie({'test1':'01', test2:'02'},{'expires':7})
if(isPlainObject(name) || (args.length>1 && name && value))
return cookieIns.set(name, value, options);
// cookie(['test1', 'test2'], null)
if(value===null) return cookieIns.remove(name);
return cookieIns.all();
}
// 提供 cookie.set(),cookie.get()等调用方式(代码复用)
for(var a in Cookie.prototype) cookie[a] = Cookie.prototype[a];

使用ES6重构

使用ES6类的定义方法去重写这个工具类
源码90多行,重构后的代码70多行

function isPlainObject(v){
return !!v && (Object === v.constructor);
}
class Cookie{
get(name){
var nameEQ = name + '=', v;
var ca = document.cookie.split('; ');
for(var i in ca){
v = ca[i];
if(v.indexOf(nameEQ) === 0){
return decodeURIComponent(v.replace(nameEQ,''));
}
}
return false;
}
set(name, value, options){
if(isPlainObject(name)){
Object.keys(name).forEach(v=>this.set(v, name[v], value))
}else{
var opt = isPlainObject(options) ? options : {expires: options},
expires = opt.expires !== undefined ? opt.expires : '',
expiresType = typeof(expires), opts=[];
if(expiresType === 'string' && expires !== '') expires = new Date(expires);
if(expiresType === 'number') expires = new Date(+new Date + 1000 * 60 * 60 * 24 * expires);
opts.push(name + '=' + encodeURIComponent(value));
if(expires) opts.push('expires=' + expires.toGMTString());
opts.push('path='+(opt.path || '/'));
if(opt.domain) opts.push('domain=' + opt.domain);
if(opt.secure) opts.push('secure');
document.cookie = opts.join('; ');
}
}
clear(name){
return name ? this.remove(name) : this.remove(Object.keys(this.all()))
}
remove(names){
names = Array.isArray(names) ? names : Array.slice.call(arguments);
return names.forEach(v=> this.set(v, '', -1))
}
all(){
if(document.cookie === '') return {};
var cookies = document.cookie.split('; '), item;
return cookies.reduce((p,n)=>{
item = n.split('=');
p[decodeURIComponent(item[0])] = decodeURIComponent(item[1]);
return p;
}, {})
}
}
const cookieIns = new Cookie();
const cookie = function(name, value, options){
var args = arguments;
if(args.length === 0) return cookieIns.all();
if(args.length === 1 && name === null) return cookieIns.clear();
if(args.length === 2 && !value) return cookieIns.clear(name);
if(typeof(name) === 'string' && !value) return cookieIns.get(name);
if(isPlainObject(name) || (args.length>1 && name && value)) return cookieIns.set(name, value, options);
if(value===null) return cookieIns.remove(name);
return cookieIns.all();
}
// 由于ES6生成的类和实例都不可遍历,但可以读取,可以采用下面的方法
Array('get', 'all', 'set', 'remove', 'clear').forEach(v=>{
cookie[v] = Cookie.prototype[v]
});