【源码共读】在前端如何操作 Cookie

简介: 【源码共读】在前端如何操作 Cookie


前端操作Cookie的场景其实并不多见,Cookie也因为各种问题被逐渐淘汰,但是我们不用Cookie也可以学习一下它的思想,或者通过这次的源码来学习其他的一些知识。


今天带来的是:js-cookie


源码分析


使用


根据README,我们可以看到js-cookie的使用方式:

// 设置
Cookies.set('name', 'value');
// 设置过期时间
Cookies.set('name', 'value', { expires: 7 })
// 获取
Cookies.get('name') // => 'value'
// 获取所有
Cookies.get() // => { name: 'value' }
// 获取指定域名下
Cookies.get('foo', { domain: 'sub.example.com' })
// 删除
Cookies.remove('name')

还有很多其他用和配置说明,大家可以自己去看看。


源码


js-cookie的源码并不多,src目录下的api.mjs就是我们要分析的源码,只有一百行左右。

/* 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的源码并不多,我们先来看看导出的是什么:


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

这里是直接导出了init函数的返回值,我们来看看init函数的返回值:

function init (converter, defaultAttributes) {
  // ...
  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) }
    }
  )
}

这里是使用Object.create创建了一个对象,这个对象有setgetremovewithAttributeswithConverter这几个方法,这几个方法都是在init函数内部定义的,我们来看看这几个方法的实现:


set

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)
}

一行一行来看:

if (typeof document === 'undefined') {
    return
}

首先判断是否有document对象,如果没有则直接返回,这说明js-cookie只能在浏览器环境下使用。

attributes = assign({}, defaultAttributes, attributes)

然后合并配置项,将defaultAttributes和传入的attributes合并,这里的assign大家直接理解为Object.assign就好了。

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

然后判断expires是否是一个数字,如果是数字则将其转换为一个Date对象;


这里的864e5是一个常量,结尾的e5代表后面加5个0,也就是86400000表示一天的毫秒数。


然后判断expires是否存在,如果存在则将其转换为UTC时间。

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

这里对name进行了编码,然后将name中的%()进行了转义。


escape是一个内置函数,它的作用是将一个字符串转换为UTF-8编码的字符串,这里的escape是将()转换为%28%29


参考: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]
}

这里是将attributes转换为字符串,这里的stringifiedAttributes是一个字符串,最后的结果是这样的:

stringifiedAttributes = '; path=/; expires=Wed, 21 Oct 2015 07:28:00 GMT'

最后将namevaluestringifiedAttributes拼接起来,然后赋值给document.cookie


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方法的实现比较简单,主要是解析document.cookie,然后将其转换为一个对象,来逐行解析:

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

对比于set方法,这里多了一个(arguments.length && !name)的判断,这里是防止传入空字符串的name

var cookies = document.cookie ? document.cookie.split('; ') : []

这里是将document.cookie分割为一个数组,每一项是一个cookie

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

这一步是只要cookienamevalue,其他的一些额外附加信息都不需要。

try {
    var found = decodeURIComponent(parts[0])
    jar[found] = converter.read(value, found)
    if (name === found) {
        break
    }
} catch (e) {
}

这里是将name进行了解码,然后将namevalue存储到jar对象中,如果传入了name,则在找到对应的name后就跳出循环。

return name ? jar[name] : jar

最后返回jar对象,如果传入了name,则返回对应的value,否则返回整个jar对象。


这里的核心是converter.read,这个方法是用来解析value的,这里的converter是一个对象,它有两个方法:

/* eslint-disable no-var */
export default {
  read: function (value) {
    if (value[0] === '"') {
      value = value.slice(1, -1)
    }
    return value.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent)
  },
  write: function (value) {
    return encodeURIComponent(value).replace(
      /%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,
      decodeURIComponent
    )
  }
}
/* eslint-enable no-var */

read方法是将value进行解码,write方法是将value进行编码。


remove

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

remove方法就是使用set方法将value设置为空字符串,然后将expires设置为-1,这样就相当于删除了cookie


withAttributes & withConverter

Object.create({
    withAttributes: function (attributes) {
        return init(assign({}, defaultAttributes, attributes))
    },
    withConverter: function (converter) {
        return init(assign({}, defaultConverter, converter))
    }
})

这两个方法就是用来设置defaultAttributesdefaultConverter的,这两个对象是用来设置cookie的默认属性和默认的converter


总结


通过学习js-cookie的源码,我们可以了解到cookie的基本使用,如果想深入了解cookie,可以参考MDN


同时我们也学会了很多字符串的处理方法,比如encodeURIComponentdecodeURIComponentsplitjoin等等。


目录
相关文章
|
4月前
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
388 1
|
5月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
371 70
|
4月前
|
存储 前端开发 JavaScript
|
6月前
|
前端开发 算法 NoSQL
前端uin后端php社交软件源码,快速构建属于你的交友平台
这是一款功能全面的社交软件解决方案,覆盖多种场景需求。支持即时通讯(一对一聊天、群聊、文件传输、语音/视频通话)、内容动态(发布、点赞、评论)以及红包模块(接入支付宝、微信等第三方支付)。系统采用前后端分离架构,前端基于 UniApp,后端使用 PHP 框架(如 Laravel/Symfony),配合 MySQL/Redis 和自建 Socket 服务实现高效实时通信。提供用户认证(JWT 集成)、智能匹配算法等功能,助力快速上线,显著节约开发成本。
132 0
前端uin后端php社交软件源码,快速构建属于你的交友平台
|
5月前
|
监控 前端开发 小程序
陪练,代练,护航,代打小程序源码/前端UNIAPP-VUE2.0开发 后端Thinkphp6管理/具备家政服务的综合型平台
这款APP通过技术创新,将代练、家政、娱乐社交等场景融合,打造“全能型生活服务生态圈”。以代练为切入点,提供模块化代码支持快速搭建平台,结合智能匹配与技能审核机制,拓展家政服务和商业管理功能。技术架构具备高安全性和扩展性,支持多业务复用,如押金冻结、录屏监控等功能跨领域应用。商业模式多元,包括交易抽成、增值服务及广告联名,同时设计跨领域积分体系提升用户粘性,实现生态共生与B端赋能。
411 12
|
9月前
|
机器学习/深度学习 前端开发 算法
婚恋交友系统平台 相亲交友平台系统 婚恋交友系统APP 婚恋系统源码 婚恋交友平台开发流程 婚恋交友系统架构设计 婚恋交友系统前端/后端开发 婚恋交友系统匹配推荐算法优化
婚恋交友系统平台通过线上互动帮助单身男女找到合适伴侣,提供用户注册、个人资料填写、匹配推荐、实时聊天、社区互动等功能。开发流程包括需求分析、技术选型、系统架构设计、功能实现、测试优化和上线运维。匹配推荐算法优化是核心,通过用户行为数据分析和机器学习提高匹配准确性。
647 5
|
11月前
|
缓存 Java Spring
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
文章比较了在Servlet和Spring Boot中获取Cookie、Session和Header的方法,并提供了相应的代码实例,展示了两种方式在实际应用中的异同。
968 3
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
|
11月前
|
JSON 前端开发 Java
震惊!图文并茂——Java后端如何响应不同格式的数据给前端(带源码)
文章介绍了Java后端如何使用Spring Boot框架响应不同格式的数据给前端,包括返回静态页面、数据、HTML代码片段、JSON对象、设置状态码和响应的Header。
438 2
震惊!图文并茂——Java后端如何响应不同格式的数据给前端(带源码)
|
11月前
|
存储 前端开发 Java
验证码案例 —— Kaptcha 插件介绍 后端生成验证码,前端展示并进行session验证(带完整前后端源码)
本文介绍了使用Kaptcha插件在SpringBoot项目中实现验证码的生成和验证,包括后端生成验证码、前端展示以及通过session进行验证码校验的完整前后端代码和配置过程。
1573 0
验证码案例 —— Kaptcha 插件介绍 后端生成验证码,前端展示并进行session验证(带完整前后端源码)
|
11月前
|
前端开发 JavaScript 小程序
前端uni开发后端用PHP的圈子系统该 如何做源码?
圈子系统系统基于TP6+Uni-app框架开发;客户移动端采用uni-app开发,管理后台TH6开发。系统支持微信公众号端、微信小程序端、H5端、PC端多端账号同步,可快速打包生成APP