js-cookie源碼學習

這篇文章最初發表在我本身搭建的站點js-cookie庫源碼學習html

背景

最近在作項目的時候,前端登陸功能要作一個記住密碼的功能。但開發用的框架中沒有實現這個功能,因此我就想本身實現這個功能。實現起來其實很簡單,就是每次用戶在登陸頁面點擊登陸時,把用戶輸入的用戶名和密碼保存到cookie中就能夠了,當用戶再登陸時,再從cookie中獲取用戶名和密碼填充到表單中就能夠了。固然,也能夠選擇保存在localStorage中,不過本文主要是想分析下用到的js-cookie這個輕量級的js庫。前端

Document.cookie

Document.cookie這個API比較"簡陋"。這裏先簡單的介紹下Document.cookie這個API,主要是寫入、讀取、刪除三個操做。git

// 1. 寫入cookie, 會追加到當前的cookie中
document.cookie = 'username=abc'
// 2. 讀取cookie,能夠用正則匹配或者字符串解析查找的方法來讀取
var usename = /username=(\S+);/.exec(document.cookie)[1] // 這裏只是簡單的匹配了下,實際開發中要考慮不少狀況
// 3. 刪除cookie,設置某個cookie過時便可
document.cookie = 'username=; expires=Thu, 01 Jan 1970 00:00:00 GMT'
// 4. 判斷是否有某個cookie,其實和讀取差很少
/username=(\S+);/.test(document.cookie) // true有,false沒有

能夠看到原生的Document.cookie寫入基本靠拼字符串,讀取和判斷是否有要靠正則或者字符串解析,刪除則也是要拼字符串。這個操做不太方便,並且和咱們日常用的API不太同樣,好比咱們經常使用的Map:github

var map = new Map();
map.set(key, value);
map.get(key);
map.has(key);
map.delete(key);

它的api就是對"鍵值對"這種數據結構進行操做。Document.cookie其實也是key=value這種鍵值對結構,可是它沒有提供像map這樣的API接口,這顯然不符合咱們的「直覺」和使用習慣。json

js-cookie

js-cookie庫使用起來很簡單,支持向瀏覽器中寫入、讀取、刪除cookie三種操做,API簡單也符合咱們的使用習慣。那麼它是如何實現這三個操做的呢?這就要分析它的源碼了。
注意:這裏分析的是它的releases-v2.2.0版本。github上的master分支源碼已經修改,主要是把set和get方法重構了,拆分紅了兩個單獨的方法,releases-v2.2.0版本set和get方法是寫在一塊兒的。下面簡單分析下它的源碼實現:set和get。remove是set的一種特殊狀況,增長expires屬性便可,這裏就不細說了。api

set的主要邏輯 -- releases-v2.2.0版本

function api (key, value, attributes) {
  // set 主要邏輯
  if (arguments.length > 1) {
  attributes = extend({
    path: '/'
  }, api.defaults, attributes);

  if (typeof attributes.expires === 'number') {
    var expires = new Date();
    expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5);
    attributes.expires = expires;
  }

  // We're using "expires" because "max-age" is not supported by IE
  attributes.expires = attributes.expires ? attributes.expires.toUTCString() : '';

  try {
    // 這裏須要注意,用戶有可能要保存json格式的cookie值,因此這裏須要轉化下
    result = JSON.stringify(value);
    if (/^[\{\[]/.test(result)) {
      // 若是轉化成了合法的json,則將value從新賦值爲json字符串,若是不含有{或[,則不是json字符串,也就不會走這個分支
      value = result;
    }
  } catch (e) {}

  // 這裏爲何要把value先編碼再解碼呢?,下面的key也是,不過key要解碼的unicode值少些
  if (!converter.write) {
    // 內置的編碼轉換
    // ["#", "$", "&", "+", ":", "<", ">", "=", "/", "?", "@", "[", "]", "^", "`", "{", "}", "|"]
    value = encodeURIComponent(String(value))
      .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
  } else {
    value = converter.write(value, key);
  }

  // 先編碼
  key = encodeURIComponent(String(key));
  // ["#", "$", "&", "+", "^", "`", "|"]
  // 再經過字符串替換的方式解碼
  key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
  // ECMA-262 standard規範已經不推薦用這個escape函數了  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/escape
  // 並且 ()也不會被編碼,因此感受下面的這句是沒有必要的
  key = key.replace(/[\(\)]/g, escape);

  // 拼接其它的cookies屬性值
  var stringifiedAttributes = '';

  for (var attributeName in attributes) {
    if (!attributes[attributeName]) {
      continue;
    }
    stringifiedAttributes += '; ' + attributeName;
    if (attributes[attributeName] === true) {
      continue;
    }
    stringifiedAttributes += '=' + attributes[attributeName];
  }
  return (document.cookie = key + '=' + value + stringifiedAttributes);
  }
}

set方法在寫入cookie時會先對cookie值encodeURIComponent而後再decodeURIComponent,這樣能夠保證存儲再cookie中的值始終是不編碼的可讀性性好的字符串。瀏覽器

get的主要邏輯 -- releases-v2.2.0版本

function api (key, value, attributes) {
  // Read

  if (!key) {
    result = {};
  }

  // 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 "get()"
  var cookies = document.cookie ? document.cookie.split('; ') : [];
  var rdecode = /(%[0-9A-Z]{2})+/g;
  var i = 0;

  for (; i < cookies.length; i++) {
    var parts = cookies[i].split('=');
    var cookie = parts.slice(1).join('=');

    if (!this.json && cookie.charAt(0) === '"') {
      // 沒看懂爲何須要這個if分支
      cookie = cookie.slice(1, -1); // 去掉了 " "
    }

    try {
      var name = parts[0].replace(rdecode, decodeURIComponent);
      cookie = converter.read ?
        converter.read(cookie, name) : converter(cookie, name) ||
        cookie.replace(rdecode, decodeURIComponent);

      if (this.json) {
        try {
          cookie = JSON.parse(cookie);
        } catch (e) {}
      }

      if (key === name) {
        result = cookie;
        break;
      }

      if (!key) {
        result[name] = cookie;
      }
    } catch (e) {}
  }

  return result;
}

整個庫暴露接口的方式 -- releases-v2.2.0版本

經過上面的get、set以及下面的代碼能夠看到整個庫其實返回的就是 function api() {}這個函數,
set、get、remove的邏輯都寫在api這個函數裏了,因此api這個函數看起來很長,並且因爲其中涉及到許多細節處理致使邏輯也比較複雜,那麼庫的做者應該是意識到了這一點,因此在github的master分支上的代碼已經把get和set拆分開了,感興趣的能夠去看看js-cookie@mastercookie

function init(converter) {
  function api (key, value, attributes) {
      api.set = api; // Cookie.set(key, value, attributes)
      api.get = function (key) { // Cookie.get(key)
        return api.call(api, key);
      };
      api.getJSON = function () { // Cookie.getJSON(key)
        return api.apply({
          json: true
        }, [].slice.call(arguments));
      };
      api.defaults = {};

      api.remove = function (key, attributes) { // Cookie.remove(key, attributes)
        api(key, '', extend(attributes, {
          expires: -1
        }));
      };

      api.withConverter = init; // 支持自定義的編碼方式

      // 返回的是 function api() {}
      return api;
  }
  // 返回的是 function api() {}
  return init(function () {});
}

js-cookie庫AMD規範和CommonJS規範的實現

js-cookie聲稱支持AMD規範和CommonJS規範,那麼它是如何支持這兩種規範的呢?數據結構

;(function (factory) {
    var registeredInModuleLoader = false;
    if (typeof define === 'function' && define.amd) { // AMD
        define(factory);
        registeredInModuleLoader = true;
    }
    if (typeof exports === 'object') { // CommonJS
        module.exports = factory();
        registeredInModuleLoader = true;
    }
    if (!registeredInModuleLoader) { // 瀏覽器
        var OldCookies = window.Cookies;
        var api = window.Cookies = factory();
        api.noConflict = function () {
            window.Cookies = OldCookies;
            return api;
        };
    }
}(function () {
  // factory 邏輯...

  // 返回的是 function api() {}
    return init(function () {});
});

其實也很簡單,如今不少js庫基本都是這麼實現的。app

總結

js-cookie庫的源碼很簡單,可是它的v2.2.0-release版本將set和get邏輯都寫在一個方法的方式實際上是不提倡這麼作的。這麼作須要用if分支來作不少過濾,會致使代碼邏輯較複雜。上面也看到,暴露接口時,它用到了function.call()和function.apply(),以及代碼內部用到的已經不推薦使用的arguments,這都使代碼的可讀性下降了。不過它裏面對於cookie的處理過程仍然值得參考。其實MDN上也提供了一個輕量的處理cookie的庫一個完整支持unicode的cookie讀取/寫入器,有時間也能夠看看。

參考

MDN: Document.cookie
GitHub: js-cookie
AMD 異步模塊定義
CommonJS

相關文章
相關標籤/搜索