js.cookie 源码学习

90 阅读3分钟

前言

代码结构

源码地址:js-cookie

源码文件地址:js-cookie/src/api.mjs

function init (converter, defaultAttributes) {
 function set (name, value, attributes) {
   // ...设置cookie
 }
 function get (name) {
   // ...获取cookie
 }
 function remove (name,attributes) {
   // ...处理remove
 }
 function withAttributes (attributes) {
   // ...属性拓展
 }
 function withConverter (converter) {
   // ...自定义解码函数拓展
 }

 return Object.create({
    set,
    get,
    remove,
    withAttributes,
    withConverter
  }, {
    attributes: { value: Object.freeze(defaultAttributes) },
    converter: { value: Object.freeze(converter) }
  })
}

export default init(defaultConverter, { path: '/' })

核心是提供 get(key)set(key, value)remove(key) 3 个 API,同时提供配置中心以支持属性修改和自定义解码函数

set

一个 cookie 不只有 keyvalue,还有 expires 过期时间以及 path 路径。一个完整的 cookie 应该长这样

${key}=${value}; expires=${expires}; path=${path}

这里不希望 set 函数的入参变得很冗余,所以这里的入参设计为:key, value, attributes 3 个。其中,attributes 是个对象,里面为 cookie 的属性:expires, path,同时为了方便使用设置了 defaultAttributes

先看代码:

function set (name, value, attributes) {
    if (typeof document === 'undefined') {
      return
    }

    attributes = assign({}, defaultAttributes, attributes)

    if (typeof attributes.expires === 'number') {
      attributes.expires = new Date(Date.now() + attributes.expires * 864e5)
    }
    if (attributes.expires) {
      attributes.expires = attributes.expires.toUTCString()
    }

    name = encodeURIComponent(name)
      .replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent)
      .replace(/[()]/g, escape)

    var stringifiedAttributes = ''
    for (var attributeName in attributes) {
      if (!attributes[attributeName]) {
        continue
      }

      stringifiedAttributes += '; ' + attributeName

      if (attributes[attributeName] === true) {
        continue
      }

      // Considers RFC 6265 section 5.2:
      // ...
      // 3.  If the remaining unparsed-attributes contains a %x3B (";")
      //     character:
      // Consume the characters of the unparsed-attributes up to,
      // not including, the first %x3B (";") character.
      // ...
      stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]
    }

    return (document.cookie =
      name + '=' + converter.write(value, name) + stringifiedAttributes)
  }

代码不是很难理解,首先对 expires 做了转成 UTC 时间戳的处理,然后把 attributes 拍扁成一个 string,最后追加到 ${key}=${value} 后面。

其中 new Date(Date.now() + attributes.expires * 864e5)864e5是 24 小时的毫秒值,具体可见 Stackoverflow

name 也使用encodeURIComponent编码处理

get

function get (name) {
    if (typeof document === 'undefined' || (arguments.length && !name)) {
      return
    }

    // To prevent the for loop in the first place assign an empty array
    // in case there are no cookies at all.
    var cookies = document.cookie ? document.cookie.split('; ') : []
    var jar = {}
    for (var i = 0; i < cookies.length; i++) {
      var parts = cookies[i].split('=')
      var value = parts.slice(1).join('=')

      try {
        var found = decodeURIComponent(parts[0])
        jar[found] = converter.read(value, found)

        if (name === found) {
          break
        }
      } catch (e) {}
    }

    return name ? jar[name] : jar
 }

get函数核心做了以下几件事

1、读取 document.cookie 然后再把 document.cookie转成数组

2、循环数组,处理每个 cookie,分离keyvalue,其中 key使用 decodeURIComponent 做了解码处理

3、cookie 的值有可能里会有 '=' 号,所以split('=') 后的,还要再 join('=') 一下变回原来的值。比如:a=123=456join 后的 value 还是 123=456 而不是 123

4、返回结果,如果key没传则认为是获取cookie列表

decodeURIComponent

编码与解码

为了 cookie 不受一些特殊字符的干扰,这里还要需要对 cookie 的值做编码与解码的工作。

cookie 的规范参考: RFC 2109 RFC 2965RFC6265

这里使用 encodeURIComponent decodeURIComponent 做编码和解码的工作。

remove

这里就比较简单了,就是调用 set 处理即可

function (name, attributes) {
  set(
    name,
    '',
    assign({}, attributes, {
      expires: -1
    })
  )
}

配置中心

为了方便操作expires以及支持自定义编解码函数(为了安全)需要提供初始化配置中心入口,于是这里暴露两个函数

 withAttributes: function (attributes) {
   return init(this.converter, assign({}, this.attributes, attributes))
 },
 withConverter: function (converter) {
   return init(assign({}, this.converter, converter), this.attributes)
 }

两个函数都生成新的对象,不会影响全局

把配置项“冻结”

最后就是防止默认配置被改动导致行为不一致,所以冻结配置

{
  attributes: { value: Object.freeze(defaultAttributes) },
  converter: { value: Object.freeze(converter) }
}

总结

  1. set 函数:把 attributes stringify,然后追加到 key=value 后, document.cookie = ${key}=${value}${attrStr}
  2. get 函数:将 document.cookie 字符串转化为 Object 形式,转化过程中判断是否在存 key,如果有就返回对应 value
  3. remove 函数:调用 set,把 expires 设置为 -1 天,cookie 直接过期被删除
  4. 默认使用 encodeURIComponentdecodeURIComponent 处理编解码问题
  5. withAttributes 自定义属性配置,并返回全新的操作 Cookie 的对象
  6. withConverter 自定义解码函数配置,并返回全新的操作 Cookie 的对象
  7. 为了解决全局污染的问题,需要把 attributesconverter 两个配置存到函数参数里,并且通过 withAttributeswithConverter 调用 init 返回新 Cookie 对象