「源码浅析」为什么要用 js-cookie 帮助开发?

396 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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!

刨根问底 —— 源码解析

微信截图_20221002161958.png

先看看整个结构,这有利于我们后面的分析。

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 的逻辑实际上就是将特定选项下给定 namecookie 失效时间设置为 -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 进行过期时间等自定义配置来满足特殊需求,同时其中的一些错误处理也是值得我们去学习的。