Cookie 相关的知识可查阅 MDN Document.cookie 文档。此篇文章通过学习浏览器最流行的 js-cookie 库,学习一下别人的设计思想。
目前为止,生产环境中使用的还是 v2.2.1 版本,当我看源码时已经是 v3.0.3-rc.0 版本了,所以,下面源码学习是基于 v3.0.3-rc.0 版本。
作者对 js-cookie 库的设计非常的巧妙,值得学习。但是整体的代码量并不是特别多。
目录
核心代码是放在 src 目录下,该目录共有三个文件:
- src
- api.mjs // 核心文件(入口)
- assign.mjs // 合并对象属性的工具方法
- converter.mjs // 默认的解码编码实现,支持用户自定义覆盖
在看 api.mjs 之前,先熟悉一下 assign.mjs 和 converter.mjs 的作用。
assign.mjs
export default function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i]
for (var key in source) {
target[key] = source[key]
}
}
return target
}
Object.assign 拷贝源对象可枚举的属性到目标对象,但是原型上可枚举属性无法拷贝。js-cookie 实现的 assign 方法可以将源对象自身可枚举属性和原型对象上可枚举的属性都可以拷贝到目标对象。
converter.mjs
export default {
read: function (value) {
return value.replace(/%3B/g, ';')
},
write: function (value) {
return String(value).replace(/;/g, '%3B')
}
}
这是 js-cookie 默认提供的编解码转换器。其作用是对存入和读取的 cookie 值进行编码和解码,因为浏览器存储 cookie 的值时不能包含任何逗号、分号或空格,所以需要进行编解码操作。还支持自定义编解码规则。
核心代码
整体设计
通过 import Cookie from "js-cookie"
引入的是 init 函数执行的返回值。get、set 读取和设置 cookie 的核心方法都保存在 init 函数作用域闭包中。
init 函数调用返回一个使用 Object.create() 创建的一个新对象。
通过 Object.create(proto [, propertiesObject]) 实现了类继承,第一个参数作为新对象的原型对象,第二个参数为这个新对象自身不可枚举的属性。
所以,引入的 Cookie 对象结构如下:
{
attributes: Object,
converter: Object,
__proto__: {
set: Function,
get: Function,
remove: Function,
withAttributes: Function,
withConverter: Function
}
}
init 初始化
import assign from './assign.mjs'
import defaultConverter from './converter.mjs'
export default init(defaultConverter, { path: '/' })
init 初始化时,提供了默认的转换器(defaultConverter)和默认可选属性 path,path 是设置 cookie 时的生效 URL,这些属性支持重写和覆盖,后文的 withConverter 和 withAttributes 会讨论。
set(key, value[,attributes])
function init (converter, defaultAttributes) {
// init 作用域闭包中的 set 函数
function set (key, value, attributes) {
// 如果存在 cookie 的可选属性对象,和默认可选属性对象合并
attributes = assign({}, defaultAttributes, attributes)
// 如果可选属性对象中设置了 expires 属性,进行一次格式化转 UTC 格式
if (typeof attributes.expires === 'number') {
attributes.expires = new Date(Date.now() + attributes.expires * 864e5)
}
if (attributes.expires) {
attributes.expires = attributes.expires.toUTCString()
}
// 对 key 使用默认的编码,防止有 ; 和 = 特殊字符
key = defaultConverter.write(key).replace(/=/g, '%3D')
// 对 value 使用转换器编码
value = converter.write(value, key)
// 将设置的键值对 key、value 和 attributes 拼接成符合规范 Document.cookie 存储的的字符串
// 例:"someCookieName=true; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/"
var stringifiedAttributes = ''
for (var attributeName in attributes) {
if (!attributes[attributeName]) {
continue
}
stringifiedAttributes += '; ' + attributeName
if (attributes[attributeName] === true) {
continue
}
stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]
}
// 存储成功后,返回结果
return (document.cookie = key + '=' + value + stringifiedAttributes)
}
}
值得注意的是 value = converter.write(value, key)
,如果使用者没有提供自定义写入编码方式,则使用 js-cookie 库默认的编码方式。具体如何自定义写入编码方式,看下文的 withAttributes 和 withConverter。
get(key)
function init (converter, defaultAttributes) {
function get (key) {
// 获取当前站点下所有 cookie 数据,切割转化为数组格式
var cookies = document.cookie ? document.cookie.split('; ') : []
// 缓存每一个 cookie 的键值对
var jar = {}
// 循环 cookies 数组,对每次循环的 key 和 value 分别进行解码
for (var i = 0; i < cookies.length; i++) {
var parts = cookies[i].split('=')
var value = parts.slice(1).join('=')
var foundKey = defaultConverter.read(parts[0]).replace(/%3D/g, '=')
// 缓存每一个 cookie 的 key 和 value
jar[foundKey] = converter.read(value, foundKey)
// 如果 cookies 中存在要获取的 cookie 的 key,提前终止循环
if (key === foundKey) {
break
}
}
// 从 jar 映射表中找到 key 对应的 value 返回
return key ? jar[key] : jar
}
}
值得注意是 document.cookie 获取的当前网站下所有的 cookie。还有注意在解码 key 时使用的默认的转换器(defaultConverter),在解码 value 时使用的可能是用户自定义的转换器(converter)或默认的转换器(defaultConverter)。
用户自定义的转换器仅仅针对 value ,key 是内部的一套编解码规则。
remove(key [, attributes])
function remove (key, attributes) {
set(
key,
'',
assign({}, attributes, {
expires: -1
})
)
}
删除 cookie 通过 set 方法将删除 key 有效期设置为 -1,即自动过期。
自身属性 attributes 和 converter
内部通过 Object.freeze() 方法将这两个属性冻结,在使用者角度是无法被修改,无法添加新属性,不能删除这两个属性,不能修改这两个属性的可枚举型、可配置型、可写性,以及不能修改已有属性的值,而且这两个冻结属性对象的原型也不能被修改。所以,站在使用者角度,这两个属性的用处不是特别大。
因为这两个属性给内部使用的。具体的应用场景在 withAttributes 和 withConverter 方法中。
以上对 set、get 和 remove 三个最常用且是最核心的方法和两个自身属性都分析了。接下来聊一下 3.x 版本新增的 API 作用和使用场景。
withAttributes、withConverter 应用场景
withAttributes
当设置两个 cookie 时,第一个 cookie 的有效期为 60s,第二个 cookie 的有效期为 30s。在 2.x 版本时可能这样写:
import Cookies from "js-cookie"
Cookie.set("cookie1", "value1", {
expires: 60
})
Cookie.set("cookie2", "value2", {
expires: 60
})
当提供了 withAttributes 和 withConverter 时,可以通过 withAttributes(attributes) 创建一个新的 cookie 对象,并分别设置默认可选属性值。
import Cookies from "js-cookie"
const cookie1 = Cookies.withAttributes({ expires: 60 })
const cookie2 = Cookies.withAttributes({ expires: 30 })
cookie1.set("c1", "v1")
cookie1.set("c3", "v3")
cookie2.set("c2", "v2")
当使用 cookie1 对象设置的任何 cookie 值,如 "c1=v1" 和 "c3=v3" 的有效期都是 60s。cookie2 设置的 "c2=v2" 有效期是 30s。
withAttributes 实现
withAttributes: function (attributes) {
return init(this.converter, assign({}, this.attributes, attributes))
}
实现很巧妙,无法言语形容,自行体会。将最新设置的可选属性和已有的可选属性合并后,再调用 init 方法初始化新的 cookie 对象,且合并后的最新可选属性作为 init 的第二个参数传入。
withConverter
和 withAttributes 使用场景一致,分别自定义 cookie 值存储的编解码方式。当设置两个 cookie 时。第一个 cookie 的 value 使用 encodeURI 编码,第二个 cookie 的 value 使用 encodeURIComponent 编码,代码如下:
import Cookies from "js-cookie"
// 自定义 cookie1 的编解码规则
const cookie1 = Cookies.withConverter({
read(value, key) {
return encodeURI(value)
},
write(value, key) {
return decodeURI(value)
}
})
// 自定义 cookie2 的编解码规则
const cookie2 = Cookies.withConverter({
read(value, key) {
return encodeURIComponent(value)
},
write(value, key) {
return decodeURIComponent(value)
}
})
cookie1.set("c1", "v1")
cookie2.set("c2", "v2")
"c1=v1" 中 "v1" 值存储的是 encodeURI 编码后结果,"c2=v2" 中 "v2" 值存储的是 encodeURIComponent 编码后的结果。读取时分别进行对应的解码。
withConverter 实现
withConverter: function (converter) {
return init(assign({}, this.converter, converter), this.attributes)
}
和 withAttributes 思路一样。
总结
以上是对 js-cookie 的学习,如理解有误敬请勘正。对 Object.create()、Object.assign() 、Object.freeze() 和对象函数的封装有了一定的了解。