MDN 上提供了操作 Cookie 的若干个例子,也有一个简单的 cookie 框架,今天尝试分析一下,最后是 jquery-cookie 插件的分析。

document.cookie 的操作例子

例 1 :简单使用

为了能直接在控制台调试,我小改成了 IIEF :

document.cookie = "name=oeschger";
document.cookie = "favorite_food=tripe";
(function alertCookie() {
alert(document.cookie);
})();

这一段是最直观地展现 document.cookie 是存取属性(accessor property)接口的例子。我们给 cookie 接口赋值,但最终效果是新增一项 cookie 键值对而不是替换。

例 2 :获取名为 test2 的 cookie 值

如果我们直接读取 document.cookie 接口,会得到当前生效的所有 cookie 信息,显然多数情况下不是我们想要的。我们可能只想读取特定名字的 cookie 值,一个最普通的想法就是用正则匹配出来:

document.cookie = "test1=Hello";
document.cookie = "test2=World"; var cookieValue = document.cookie.replace(/(?:(?:^|.*;\s*)test2\s*\=\s*([^;]*).*$)|^.*$/, "$1"); (function alertCookieValue() {
alert(cookieValue);
})();

这里用了两次非捕获组和一次捕获组,最终获取的是捕获组中的 cookie 值。首先 (?:^|.*;\s*) 定位到 test2 开始处,它可能是在整个 cookie 串的开头,或者躲在某个分号之后;接着 \s*\=\s* 不用多说,就是为了匹配等号;而 ([^;]*) 则是捕获不包括分号的一串连续字符,也就是我们想要的 cookie 值,可能还有其他的一些 cookie 项跟在后面,用 .*$ 完成整个匹配。最后如果匹配不到我们这个 test2 也就是说根本没有名为 test2 的 cookie 项,捕获组也就是 $1 会是空值,而 |^.*$ 巧妙地让 replace 函数把整个串都替换成空值,指示我们没有拿到指定的 cookie 值。

那有没有别的方法呢?考虑 .indexOf() 如果别的某个 cookie 项的值也包含了要查找的键名,显然查找位置不符合要求;最好还是以 ; 分割整个串,遍历一遍键值对。

例 3 :让某个操作只做一次

通过在执行某个操作时维护一次 cookie ,之后读取 cookie 就知道该操作是否已经执行过,决定要不要执行。

当然我们这个 cookie 它不能很快过期,否则维护的信息就很快丢失了。

(function doOnce() {
if (document.cookie.replace(/(?:(?:^|.*;\s*)doSomethingOnlyOnce\s*\=\s*([^;]*).*$)|^.*$/, "$1") !== "true") {
alert("Do something here!");
document.cookie = "doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT";
}
});
doOnce();
doOnce();

还是读取 cookie ,这里判断下特定的 cookie 值是不是指定值比如 true ,执行某些操作后设置 cookie 且过期时间视作永久。另外要注意 expires 是 UTC 格式的。

例 4 :重置先前的 cookie

比如在例 3 中的 cookie 我想重置它,以便再执行一遍操作;或者就是想删除某项 cookie :

(function resetOnce() {
document.cookie = "doSomethingOnlyOnce=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
})();

通过将 expires 设置为“元日”(比如 new Date(0) ),或者设置 max-age 为 -1 也可,会让 cookie 立即过期并被浏览器删除。但要注意 cookie 的时间是基于客户机的,如果客户机的时间不正确则可能删除失败,所以最好额外将 cookie 值也设为空。

例 5 :在 path 参数中使用相对路径

由于 path 参数是基于绝对路径的,使用相对路径会出错,我们需要手动地转换一下。 JS 可以很方便地使用正则表达式替换:

/*\
|*|
|*| :: Translate relative paths to absolute paths ::
|*|
|*| https://developer.mozilla.org/en-US/docs/Web/API/document.cookie
|*| https://developer.mozilla.org/User:fusionchess
|*|
|*| The following code is released under the GNU Public License, version 3 or later.
|*| http://www.gnu.org/licenses/gpl-3.0-standalone.html
|*|
\*/ function relPathToAbs (sRelPath) {
var nUpLn, sDir = "", sPath = location.pathname.replace(/[^\/]*$/, sRelPath.replace(/(\/|^)(?:\.?\/+)+/g, "$1"));
for (var nEnd, nStart = 0; nEnd = sPath.indexOf("/../", nStart), nEnd > -1; nStart = nEnd + nUpLn) {
nUpLn = /^\/(?:\.\.\/)*/.exec(sPath.slice(nEnd))[0].length;
sDir = (sDir + sPath.substring(nStart, nEnd)).replace(new RegExp("(?:\\\/+[^\\\/]*){0," + ((nUpLn - 1) / 3) + "}$"), "/");
}
return sDir + sPath.substr(nStart);
}

首先注意到 location.pathname.replace(/[^\/]*$/, ...) ,先不管第二个参数,这里的正则是要匹配当前 pathname 末尾不包括斜杠的一串连续字符,也就是最后一个目录名。

sRelPath.replace(/(\/|^)(?:\.?\/+)+/g, "$1") 则比较巧妙,会将诸如 /// .// ././.// 的同级路径过滤成单一个斜杠或空串,剩下的自然就只有 ../ ../../ 这样的合法跳转上级的路径。两个放在一起看就是在做相对路径的连接了。

接下来则是一个替换的循环,比较简单,本质就是根据 ../ 的个数删除掉对应数量的目录名,不考虑性能的粗暴模拟算法。

还有一个奇怪的例子

真的被它的设计恶心到了,其实就是前面“只执行一次某操作”的普适版。只想分析一个 replace() 函数中正则的用法,完整代码感兴趣的可以上 MDN 看。

encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&")

根据 MDN 上 String.prototype.replace() 的说明,第二个参数还可以传以下的模式串:

$$ :插入一个 $ 符号;

$& :插入匹配到的子串;

$` :插入在匹配子串之前的部分;

$' :插入在匹配子串之后的部分;

$n :范围 [1, 100) 插入第 n 个括号匹配串,只有当第一个参数是正则对象才生效。

因此第二个参数 "\\$&" 巧妙地给这些符号做了一次反斜杠转义。

简单 cookie 框架

MDN 上也提供了一个支持 Unicode 的 cookie 访问封装,完整代码也维护在 github madmurphy/cookies.js 上。

基本骨架

只有 5 个算是增删改查的方法,全都封装在 docCookies 对象中,比较简单:

/*\
|*|
|*| :: cookies.js ::
|*|
|*| A complete cookies reader/writer framework with full unicode support.
|*|
|*| Revision #3 - July 13th, 2017
|*|
|*| https://developer.mozilla.org/en-US/docs/Web/API/document.cookie
|*| https://developer.mozilla.org/User:fusionchess
|*| https://github.com/madmurphy/cookies.js
|*|
|*| This framework is released under the GNU Public License, version 3 or later.
|*| http://www.gnu.org/licenses/gpl-3.0-standalone.html
|*|
|*| Syntaxes:
|*|
|*| * docCookies.setItem(name, value[, end[, path[, domain[, secure]]]])
|*| * docCookies.getItem(name)
|*| * docCookies.removeItem(name[, path[, domain]])
|*| * docCookies.hasItem(name)
|*| * docCookies.keys()
|*|
\*/ var docCookies = {
getItem: function (sKey) {...},
setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {...},
removeItem: function (sKey, sPath, sDomain) {...},
hasItem: function (sKey) {...},
keys: function () {...}
}; if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = docCookies;
}

源码解读

getItem 方法的实现思想其实都已经在前面有过分析了,就是利用正则匹配,需要注意对键名编码而对键值解码:

getItem: function (sKey) {
if (!sKey) { return null; }
return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
}

hasItem 方法中,会先验证键名的有效性,然后还是那个正则的匹配键名部分。。

hasItem: function (sKey) {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
}

removeItem 方法也是在前面分析过的,设置过期时间为元日并将键值和域名、路径等属性值设空;当然也要先判断一遍这个 cookie 项存不存在:

removeItem: function (sKey, sPath, sDomain) {
if (!this.hasItem(sKey)) { return false; }
document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "");
return true;
}

keys 方法返回所有可读的 cookie 名的数组。

keys: function () {
var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) { aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]); }
return aKeys;
}

这里的正则比较有意思,第一个 ((?:^|\s*;)[^\=]+)(?=;|$) 有一个非捕获组和一个正向零宽断言,能够过滤只有键值,键名为空的情况。比如这个 nokey 会被过滤掉:

document.cookie = 'nokey';
document.cookie = 'novalue=';
document.cookie = 'normal=test';
console.log(document.cookie);
// nokey; novalue=; normal=test

而第二个 ^\s* 就是匹配开头的空串;第三个 \s*(?:\=[^;]*)?(?:\1|$) 应该是匹配包括等号在内的键值,过滤掉后整个串就只剩下键名了。但这个实现不对,我举的例子中经过这样的处理会变成 ;novalue;normal ,之后 split() 就会导致第一个元素是个空的元素。可以说是为了使用正则而导致可阅读性低且有奇怪错误的典型反例了。

setItem 就是设置 cookie 的方法了,处理得也是比较奇怪, constructor 会有不同页面对象不等的情况,至于 max-age 的不兼容情况在注释里提到了,却不打算改代码。。好吧可能就为了示例?。。

setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
var sExpires = "";
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
/*
Note: Despite officially defined in RFC 6265, the use of `max-age` is not compatible with any
version of Internet Explorer, Edge and some mobile browsers. Therefore passing a number to
the end parameter might not work as expected. A possible solution might be to convert the the
relative time to an absolute time. For instance, replacing the previous line with:
*/
/*
sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; expires=" + (new Date(vEnd * 1e3 + Date.now())).toUTCString();
*/
break;
case String:
sExpires = "; expires=" + vEnd;
break;
case Date:
sExpires = "; expires=" + vEnd.toUTCString();
break;
}
}
document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
return true;
}

jquery-cookie 插件

尽管这个仓库早已不维护了,但其代码还是有可借鉴的地方的。至少没有尝试用正则去做奇怪的匹配呐。还有一个是,如果不知道迁移的原因,只看代码的话真会以为 jquery-cookie 才是从 js-cookie 来的,毕竟后者迁移后的风格没有前者优雅了, so sad..

基本骨架

嗯,典型的 jQuery 插件模式。

/*!
* jQuery Cookie Plugin v1.4.1
* https://github.com/carhartl/jquery-cookie
*
* Copyright 2006, 2014 Klaus Hartl
* Released under the MIT license
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD (Register as an anonymous module)
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS
module.exports = factory(require('jquery'));
} else {
// Browser globals
factory(jQuery);
}
}(function ($) { var pluses = /\+/g; function encode(s) {...} function decode(s) {...} function stringifyCookieValue(value) {...} function parseCookieValue(s) {...} function read(s, converter) {...} var config = $.cookie = function (key, value, options) {...}; config.defaults = {}; $.removeCookie = function (key, options) {...}; }));

在我看来,这个架构是比较友好的, encode()decode() 函数可以单独增加一些编码相关的操作,两个以 CookieValue 为后缀的函数也是扩展性比较强的, read() 可能要适当修改以适应更多的 converter 的使用情况。新的仓库则是把它们全都揉在一起了,就像 C 语言写了一个大的 main 函数一样,比较可惜。

边角函数

可以看到几个函数的处理还比较粗糙,像 encodeURIComponent() 在这里有很多不必要编码的情况,会额外增加长度。尽管如此,不难看出核心调用关系:对于 key 可能只需要简单的 encode() / decode() 就好了,而对于 value 的写入会先通过 stringifyCookieValue() 序列化一遍,读出则要通过 read() 进行解析。

    var pluses = /\+/g;

    function encode(s) {
return config.raw ? s : encodeURIComponent(s);
} function decode(s) {
return config.raw ? s : decodeURIComponent(s);
} function stringifyCookieValue(value) {
return encode(config.json ? JSON.stringify(value) : String(value));
} function parseCookieValue(s) {
if (s.indexOf('"') === 0) {
// This is a quoted cookie as according to RFC2068, unescape...
s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
} try {
// Replace server-side written pluses with spaces.
// If we can't decode the cookie, ignore it, it's unusable.
// If we can't parse the cookie, ignore it, it's unusable.
s = decodeURIComponent(s.replace(pluses, ' '));
return config.json ? JSON.parse(s) : s;
} catch(e) {}
} function read(s, converter) {
var value = config.raw ? s : parseCookieValue(s);
return $.isFunction(converter) ? converter(value) : value;
} var config = $.cookie = function (key, value, options) {...}; config.defaults = {}; $.removeCookie = function (key, options) {
// Must not alter options, thus extending a fresh object...
$.cookie(key, '', $.extend({}, options, { expires: -1 }));
return !$.cookie(key);
};

至于 $.removeCookie 则是通过 $.cookie() 设置过期时间为 -1 天来完成。这也是可以的,可能会多一丁点计算量。

核心读写 $.cookie

通过参数个数决定函数功能应该是 JS 的常态了。那么 $.cookie 也有读和写两大功能。首先是写:

    var config = $.cookie = function (key, value, options) {

        // Write

        if (arguments.length > 1 && !$.isFunction(value)) {
options = $.extend({}, config.defaults, options); if (typeof options.expires === 'number') {
var days = options.expires, t = options.expires = new Date();
t.setMilliseconds(t.getMilliseconds() + days * 864e+5);
} return (document.cookie = [
encode(key), '=', stringifyCookieValue(value),
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
options.path ? '; path=' + options.path : '',
options.domain ? '; domain=' + options.domain : '',
options.secure ? '; secure' : ''
].join(''));
} ...
}; config.defaults = {};

从功能来看, config.defaults 应该是暴露出来可以给 cookie 的一些属性维护默认值的,而传入的 options 当然也可以覆盖先前设置的默认值。这里的 typeof 判断类型似乎也是不太妥的。最后有一个亮点是 .join('') 用了数组连接代替字符串连接。

接下来是读取 cookie 的部分:

    var config = $.cookie = function (key, value, options) {

        ...

        // arguments.length <= 1 || $.isFunction(value)
// Read var result = key ? undefined : {},
// To prevent the for loop in the first place assign an empty array
// in case there are no cookies at all. Also prevents odd result when
// calling $.cookie().
cookies = document.cookie ? document.cookie.split('; ') : [],
i = 0,
l = cookies.length; for (; i < l; i++) {
var parts = cookies[i].split('='),
name = decode(parts.shift()),
cookie = parts.join('='); if (key === name) {
// If second argument (value) is a function it's a converter...
result = read(cookie, value);
break;
} // Prevent storing a cookie that we couldn't decode.
if (!key && (cookie = read(cookie)) !== undefined) {
result[name] = cookie;
}
} return result;
};

由于现代浏览器在存 cookie 时都会忽略前后空格,所以读出来的 cookie 串只需要 ;\x20 来分割。当然也可以只 ; 分割,最后做一次 trim() 去除首尾空格。

.split('=') 会导致的一个问题是,如果 cookie 值是 BASE64 编码或其他有包含 = 的情况,就会多分割,所以会有 .shift() 和再次 .join('=') 的操作。这里又分两种情况,如果指定了 key 则读取对应的 value 值,如果什么都没有指定则返回包含所有 cookie 项的对象。

嗯,大概就酱。

参考

  1. Document.cookie - Web APIs | MDN
  2. Object.defineProperty() - JavaScript | MDN
  3. Simple cookie framework - Web APIs | MDN
  4. Github madmurphy/cookies.js
  5. Github carhartl/jquery-cookie
  6. Github js-cookie/js-cookie
  7. RFC 2965 - HTTP State Management Mechanism
  8. RFC 6265 - HTTP State Management Mechanism

本文基于 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 发布,欢迎引用、转载或演绎,但是必须保留本文的署名 BlackStorm 以及本文链接 http://www.cnblogs.com/BlackStorm/p/7618416.html ,且未经许可不能用于商业目的。如有疑问或授权协商请 与我联系

几个 Cookie 操作例子的分析的更多相关文章

  1. javascrip中cookie的使用详细分析

    JavaScript中的另一个机制:cookie,则可以达到真正全局变量的要求. cookie是浏览器 提供的一种机制,它将document 对象的cookie属性提供给JavaScript.可以由J ...

  2. js中cookie的使用详细分析

    JavaScript中的另一个机制:cookie,则可以达到真正全局变量的要求. cookie是浏览器 提供的一种机制,它将document 对象的cookie属性提供给JavaScript.可以由J ...

  3. js里cookie操作

    原生js操作cookie 创建和存储 cookie 在这个例子中我们要创建一个存储访问者名字的 cookie.当访问者首次访问网站时,他们会被要求填写姓名.名字会存储于 cookie 中.当访问者再次 ...

  4. cookie操作大全

    JavaScript中的另一个机制:cookie,则可以达到真正全局变量的要求. cookie是浏览器 提供的一种机制,它将document 对象的cookie属性提供给JavaScript.可以由J ...

  5. PHP与JavaScript下的Cookie操作

    下面的例子列出几种情形交互场景,列出JS和php交互的方法.总结下,以免日后再为cookie问题困扰. setcookie.php getcookie.php 总结: php用自身函数读取php 的c ...

  6. [Angularjs]cookie操作

    摘要 现在很多app采用内嵌h5的方式进行开发,有些数据会存在webveiw的cookie中,那么如果使用angularjs开发单页应用,就需要用到angularjs的cookie操作.这里提供一个简 ...

  7. js中cookie的使用具体分析

                   JavaScript中的还有一个机制:cookie,则能够达到真正全局变量的要求. cookie是浏览器 提供的一种机制,它将document 对象的cookie属性提供 ...

  8. ReactiveCocoa 中 RACSignal 所有变换操作底层实现分析(上)

    前言 在上篇文章中,详细分析了RACSignal是创建和订阅的详细过程.看到底层源码实现后,就能发现,ReactiveCocoa这个FRP的库,实现响应式(RP)是用Block闭包来实现的,而并不是用 ...

  9. Python脚本控制的WebDriver 常用操作 <二十八> 超时设置和cookie操作

    超时设置 测试用例场景 webdriver中可以设置很多的超时时间 implicit_wait.识别对象时的超时时间.过了这个时间如果对象还没找到的话就会抛出异常 Python脚本 ff = webd ...

随机推荐

  1. [2014-08-28]Mac系统上的几个命令解释器(控制台)

    irb 语言:Ruby 帮助:help 清屏:CTRL+L 自动完成:Tab+Tab (若未开启,则在/etc/irbrc中require 'irb/completion') 退出:quit/exit ...

  2. 为table元素添加操作日志

    1.为所有的元素添加函数onchange() <input id="status" value="${status}" onchange="ch ...

  3. 手工删除crfclust.bdb文件

    环境:RHEL 6.5 + Oracle 11.2.0.4 RAC 现象:巡检发现自己的测试环境节点2的空间使用率过高,进一步查询,发现大文件是GI目录下crfclust.bdb文件. crfclus ...

  4. SVG渐变

    前面的话 给SVG元素应用填充和描边,除了使用纯色外,还可以使用渐变.本文将详细介绍SVG渐变 线性渐变 有两种类型的渐变:线性渐变和径向渐变.必须给渐变内容指定一个id属性,否则文档内的其他元素不能 ...

  5. C#:委托(delegate)和事件(event) (转)

    委托(delegate): 它是C#语言里面的函数指针,代表可以指向某一个函数,在运行的时候调用这个函数的实现.下面来看看它的实现步骤: 声明一个delegate对象. 实现和delegate具有相同 ...

  6. 【ctrl+A】与【ctrl+单击图层缩略图】有什么区别?

    如果这图层没有透明区域的话那和ctrl+A的效果是一样的! 但如果图层有不透明区域,那选中的就是图层中所有不透明的区域!

  7. 个人作业3-(Alpha阶段)

    一. 总结自己的alpha 过程 1.团队的整体情况 Alpha阶段初期我们团队因分工以及项目具体实施一度茫然,好在在团队队长的带领下确认分工及制定具体计划,使任务有序的进行下去,中间过程虽然遇到一些 ...

  8. Swing-选项卡面板JTabbedPane-入门

    注:非原创,内容源自<Swing 的选项卡面板>,笔者做了少量修改. 选项卡面板是一个很常用的Swing组件,在window下,右击我的电脑,查看属性,就是一个典型的选修卡面板.当然还有最 ...

  9. 201521123012 《Java程序设计》第五周学习总结

    ##1. 本周学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. 答: 1.2 可选:使用常规方法总结其他上课内容. 答:匿名内部类:将一个类的定义放在另一个类的内部.一般是 **new ...

  10. python之线程相关的其他方法

    一.join方法 (1)开一个主线程 from threading import Thread,currentThread import time def walk(): print('%s is r ...