持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第22天,点击查看活动详情
前言
不知道大家平时开发中有没有接触过 Cookie
?原生 Cookie
的读写以及删除是很麻烦的,相信接触过的小伙伴们都知道里头的辛酸,大家可能会选择自己封装个简单的类,统一操作。实际上业内已经有一个好用的库了 —— js-cookie。它可以很方便的对 Cookie
进行各种操作,同时还支持 Cookie
的过期时间等其他的操作。
本文带大家阅读 js-cookie
源码,了解它是怎么进行封装的。
知根知底 —— js-cookie 的用法
我们要理解源码,首先要知道它怎么用。
Cookies.set
我们看看 set
的使用方式有哪些:
设置一个 Cookie
Cookies.set('name', 'CatWatermelon')
包含失效时间
Cookies.set('name', 'CatWatermelon', { expires: 7 })
包含失效时间和路径
Cookies.set('name', 'CatWatermelon', { expires: 7, path: '' })
Cookies.get
我们看看 get 的使用方式有哪些:
获取某个 Cookie
Cookies.get('name'); // CatWatermelon
Cookies.get('name2'); // undefined
获取所有 Cookie
Cookies.get();
注:通过 Cookies.get
获取到的所有 cookie
,依然是 document.cookie
能访问到的那部分。
获取指定 domain 下的某个 Cookie
Cookies.get('foo', { domain: 'sub.example.com' })
虽然但是,这是不行滴 。
Cookies.remove
添加 Cookie 和获取 Cookie 说完了,接下来看删除 Cookie。
删除某个 Cookie
Cookies.remove('name')
删除指定路径下的 Cookie
Cookies.set('name', 'CatWatermelon', { path: '' })
Cookies.remove('name') // fail!
Cookies.remove('name', { path: '' }) // removed!
刨根问底 —— 源码解析
先看看整个结构,这有利于我们后面的分析。
init 的接收参数
首先是定义了一个 init
方法,这个方法接收 两个参数 。其中 converter
用于转换,完整代码如下:
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
}
而 defaultAttributes
是默认配置:
{
path: '/'
}
set 函数
function set(name, value, attributes) {
// 1. 非浏览器环境直接返回
if (typeof document === 'undefined') {
return
}
// 2. 合并对象属性
attributes = assign({}, defaultAttributes, attributes)
// 3. 失效时间
if (typeof attributes.expires === 'number') {
attributes.expires = new Date(Date.now() + attributes.expires * 864e5)
}
// 4. 将失效时间转为 UTC 格式 —— 'Mon, 03 Oct 2022 15:57:25 GMT'
if (attributes.expires) {
attributes.expires = attributes.expires.toUTCString()
}
// 5. 编码处理
name = encodeURIComponent(name)
.replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent)
.replace(/[()]/g, escape)
// 6.
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]
}
// 7. 返回值
return (document.cookie =
name + '=' + converter.write(value, name) + stringifiedAttributes)
}
1. 环境支持
if (typeof document === 'undefined') {
return
}
get
函数一进来首先判断是否是 浏览器环境 ,即判断 document
对象是否在当前环境中存在。
如果不是则直接以 undefined
作为返回值,否则继续。
2. 合并对象
attributes = assign({}, defaultAttributes, attributes)
将默认配置及自定义配置合并为一个新的对象赋值给变量 attributes
。
assign
的源码如下:
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
}
3. 失效时间
if (typeof attributes.expires === 'number') {
attributes.expires = new Date(Date.now() + attributes.expires * 864e5)
}
判断自定义配置中是否传入了 合法的失效时间 属性。
864e5 就是 864 * Math.pow(10, 5),即 86400000。
86400000 毫秒相当于一天。
4. 统一时间格式为 UTC 时间
if (attributes.expires) {
attributes.expires = attributes.expires.toUTCString()
}
形如 Mon, 03 Oct 2022 15:57:25 GMT
5. 对 cookie 名进行编码格式化
name = encodeURIComponent(name)
.replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent)
.replace(/[()]/g, escape)
这里对 name
进行 编码处理及格式化 , 后续 get
读取时也会进行一次 解码 操作。
6. 拼接 value 值
var stringifiedAttributes = ''
for (var attributeName in attributes) {
if (!attributes[attributeName]) {
continue
}
stringifiedAttributes += '; ' + attributeName
if (attributes[attributeName] === true) {
continue
}
stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]
}
这段代码是对 name
对应的 value
值做的拼接处理,将 expires、path 等属性与 value
进行拼接(一并存储起来)。
7. 设置值并返回
return (document.cookie = name + '=' + converter.write(value, name) + stringifiedAttributes)
这里通过 document.cookie
设置值,并将其作为返回值进行返回。
get 函数
function get(name) {
// 1. 入参检查
if (typeof document === 'undefined' || (arguments.length && !name)) {
return
}
// 2. 获取 document.cookie
// 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('; ') : []
// 3. 将 cookies 数组转为对象形式
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
}
1. 环境支持及入参检查
if (typeof document === 'undefined' || (arguments.length && !name)) {
return
}
get
函数一进来首先判断是否是 浏览器环境 ,即判断 document
对象是否在当前环境中存在。
其次,进行 入参检查 ,判断入参 name
的值是否合法(比如以空字符串、false这类作为 name)。
如果不满足则直接以 undefined
作为返回值,否则继续。
2. 获取所有 cookie 存入 cookies 数组
var cookies = document.cookie ? document.cookie.split('; ') : []
相信大家都知道可以通过 document.cookie
获取所有 能访问的 cookie 。
而调用 get
时,当前网页中是有可能不存在任何 可访问的 cookie 的,此时调用 split
进行分割,得到的会是一个 仅包含空字符串的数组 。
有的小伙伴可能会想,那空字符串就空字符串呗,咋的了。
别急,我们接着往下看先。
3. 将 cookies 数组转为对象
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) { }
}
这里将刚刚步骤 2 得到的 数组转为对象 ,而步骤 2 中,如果最后数组中包含了空串,那么在这一步,这个空串就会被当作一个属性加入到对象中,这显然是不合理的。
4. 根据入参 name 返回处理结果
return name ? jar[name] : jar
上面介绍过 get
的用法,它可以根据给定的 name
来获取对应的 cookie
,也可以省略 name
来获取所有的 cookie
。
这里就是判断有没有传入 name
来决定返回什么。
remove 函数
remove: function (name, attributes) {
set(
name,
'',
assign({}, attributes, {
expires: -1
})
)
},
这里 remove
的逻辑实际上就是将特定选项下给定 name
的 cookie
失效时间设置为 -1 来实现的。
完整源码
/* eslint-disable no-var */
import assign from './assign.mjs'
import defaultConverter from './converter.mjs'
function init(converter, 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)
}
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
}
return Object.create(
{
set: set,
get: get,
remove: function (name, attributes) {
set(
name,
'',
assign({}, attributes, {
expires: -1
})
)
},
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) }
}
)
}
export default init(defaultConverter, { path: '/' })
/* eslint-enable no-var */
结束语
本文当此就结束了,实际上通过本文我们会发现,对某一功能进行增强,我们通常的做法是,以它为核心,在它的基础上去扩充一些功能,而不破坏它原本的使用方式。从 js-cookie
的源码中,我们可以举一反三,例如对 Storage
进行过期时间等自定义配置来满足特殊需求,同时其中的一些错误处理也是值得我们去学习的。