幾個 Cookie 操作例子的分析


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 鍵值對而不是替換。

如果我們直接讀取 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 格式的。

比如在例 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 個括號匹配串,只有當第一個參數是正則對象才生效。

因此第二個參數 "\\$&" 巧妙地給這些符號做了一次反斜杠轉義。

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 才是從 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 天來完成。這也是可以的,可能會多一丁點計算量。

通過參數個數決定函數功能應該是 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 ,且未經許可不能用於商業目的。如有疑問或授權協商請 與我聯系


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM