JSON序列化与解析以及在浏览器中数据存储方式
一、JSON的由来
在目前的开发中,JSON是一种非常重要的数据格式,它并不是编程语言,而是一种可以在服务器和客户端之间传输的数据格式,它诞生于 JavaScript 语言,其名称中的 "JavaScript Object Notation"(对象符号) 表明它是一种基于 JavaScript 语法的标记格式,虽然它的语法源于 JavaScript,但它不仅限于 JavaScript,也被广泛用于不同语言之间的数据交换 在 1990 年代末和 2000 年代初,随着互联网的快速发展,浏览器与服务器之间的数据交换需求迅速增长。尤其是 Web 应用程序的兴起,需要一种轻量、可读且易于解析的方式来在客户端和服务器之间传输数据,这是JSON出现所需的土壤环境 在 JSON 出现之前,数据交换的主要方式是 XML,这也是一种结构化的数据格式,在目前的Java中,仍旧可以看到该身影,但 XML
的缺点包括格式冗长、解析复杂、可读性差。这些问题激发程序员寻找一种更为简洁的替代方案,JSON 因此应运而生Douglas Crockford 是 JSON 的主要提出者之一。在 2001 年左右,他对 JavaScript 对象字面量(即 {}
的方式)进行了扩展,提出了 JSON 的概念Crockford 观察到 JavaScript 对象的结构非常适合用于数据交换——它简单、易于理解,同时也非常符合 JavaScript 的天然结构。因此,他推动将这种格式作为一种独立的数据格式,并为其命名为 JavaScript Object Notation(JSON),当独立的那一刻,就与JS对象走向不同的发展道路,因此两者并不一致 随后JSON随着时代的发展,2006年标准化,2013年进一步更新规范为RFC 7159,2013当年被纳入ECMA规范之中,这就是JSON诞生的过程,另外一个在网络传输中目前已经越来越多使用的传输格式是protobuf,但是直到2021年的3.x版本才支持JavaScript,所以目前在前端使用的较少,所以这里不过多讲解
//XML格式
<person>
<name>coderwhy</name>
<age>35</age>
</person>
//JSON格式
{
"name": "coderwhy",
"age": 35
}
目前JSON被使用的场景也越来越多: 网络数据的传输JSON数据 项目的某些配置文件,例如小程序配置文件(甚至做出了可视化效果) 非关系型数据库(NoSQL)将json作为存储格式
图32-1 小程序的配置文件
1.1 JSON基础语法
在 JSON 中,顶层指的是 JSON 数据结构的最外层部分,也就是整个 JSON 文档的根级别,它决定了整个 JSON 数据的主要结构类型。JSON 数据的顶层可以是以下三种类型:
简单值:这是一个基础类型的值,例如一个数字、字符串、布尔值或
null
。对象值:这是用
{}
包裹的键-值对集合,通常用来表示复杂的结构或具有命名字段的数据。数组值:这是用
[]
包裹的一组值,值可以是简单值、对象、或其他数组。
顶层的概念是指,整个 JSON 文档最外面的部分必须是这三种之一
// 1. 简单值(Simple Values)
// 顶层是一个简单值:可以是数字、字符串、布尔值或 null。
// 数字
42
// 字符串(必须使用双引号)
"Hello, World!"
// 布尔值
true
// null 值
null
// 2. 对象值(Object Values)
// 顶层是一个对象:由键-值对组成,键必须是字符串并用双引号括起来,值可以是简单值、对象或数组。
{
"name": "coderwhy",
"age": 25,
"isStudent": false,
"address": {
"city": "New York",
"zip": "10001"
},
"hobbies": ["reading", "cycling", "traveling"],
"phoneNumber": null
}
// 3. 数组值(Array Values)
// 顶层是一个数组:数组中的元素可以是简单值、对象或其他数组。
[
"apple",
"banana",
"cherry"
]
[
{
"name": "XiaoYu",
"age": 20
},
{
"name": "coderwhy",
"age": 22
}
]
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
理解顶层很重要,因为JSON 的顶层结构类型决定了 JSON 文档的用途和适用场景 如果顶层是一个对象,通常表示一组相关的数据(例如一个用户、一个配置项等),这是最常见的结构 如果顶层是一个数组,通常表示多个同类型的数据的集合(例如用户列表、商品列表等) 如果顶层是一个简单值,通常用于一些单独的结果返回(例如状态码、简单消息等) 当编写代码来解析 JSON 时,处理方式会根据顶层的结构不同而变化。假如 JSON 顶层是一个对象,解析时会期待用对象的方式来访问其键-值对。如果顶层是数组,则需要用数组的方式来访问其元素。因此类型决定了 JSON 结构的复杂度和解析方式
1.2 JSON序列化
某些情况下我们希望将JavaScript中的复杂类型转化成JSON格式的字符串,这样方便对其进行处理: 比如我们希望将一个对象保存到localStorage中(暂时理解为一种本地存储) 但是如果我们直接存放一个对象,这个对象会被转化成 [object Object] 格式
的字符串,并不是我们想要的结果这是因为 localStorage.setItem
方法要求接收的value需要为string类型,而我们传入了一个对象。传入不符合要求的内容,容易出现不可预测的事情,但好在这里较为明显,是以往我们遇到过的情况,在需要字符串而传入非字符串时,会被强制通过toString转为字符串,对象被toStirng方法强转的表现行为是[object Object]
const info = {
name: "coderwhy",
age: 18,
friends: {
name: "XiaoYu"
},
hobbies: ["篮球", "足球"]
}
// 将对象数据存储localStorage,setItem方法的参数1为key,参数2为value
localStorage.setItem("info", info)
图32-2 浏览器中localStorage的存储表现
强转的结果非理想预期,因为具备实际意义的Value值丢失了,传入的是对象本身强转的结果,而非对象内容,当我想取出value时,就只能拿到[object Object]
因此在传入时,我们需要先将info对象人为控制的转为字符串,以防出现强转结果,那我们要如何合理转为字符串而不丢失对象内容呢?
首先对象原型身上的 toString方法
是绝对不行的,我们需要避开的就是该情况这时需要利用上JSON格式,由于JSON顶层接受 简单值
,string字符串类型就属于简单值,而且经过JSON转化后想要读取也会更加简单那我们要如何转化为JSON格式呢?这就需要说到来自JS本身的JSON内置对象了,它提供了"序列化"的方式,也被称为序列化方法
1.2.1 什么是序列化
JSON 序列化(Serialization)是指将数据(通常是编程语言中的对象、数组或其他数据结构)转换为JSON格式。这个过程将数据结构转换成字符串,从而做到数据能够被保存、传输或在不同的系统之间交换
序列化非常重要,因为计算机程序内部的数据通常是复杂的内存结构(如对象、列表等),这些结构不能直接通过网络传输或者直接保存到磁盘。通过序列化,我们将这些复杂的数据结构转换为一种标准的、可传输的字符串格式 这种做法很有意思,所有被 JSON字符串
包裹的类型都会变成字符串,在计算机眼里都会失去原本的含义,在我们看来无非是披上一层字符串马甲,结构的表达还是一样的,但在计算机的眼中,字符串包裹起来的内容只能算是"文本",顶多就是一个长得和对象一样的"文本",但底层本质已经是改变了,它是一个JSON格式的字符串,由于 JSON 是标准化的,几乎所有编程语言都能解析 JSON,因此它在系统之间的数据交换中非常常用因此诞生了一种很实用的技巧,我们可以传输的时候套上马甲,使其失去"意义",从而做到能在多个地方进行交换,等交换结束,再把这层马甲脱掉
1.2.2 序列化方法
JSON 是一种语法,用来序列化对象、数组、数值、字符串、布尔值和 null
。它基于 JavaScript 语法,但与之不同:大部分 JavaScript 不是 JSON
在JS中有一个标准内置对象JSON,提供的两个静态方法 JSON.Stringify
和JSON.parse
很有用,我们来学习一下JSON.stringify()
方法将一个 JavaScript 对象或值转换为 JSON 字符串
//之前学习过的语法
//value:将要序列化成 一个 JSON 字符串的值
JSON.stringify(value[, replacer [, space]])
此时对我们一开始的info对象进行JSON序列化,再存储进localStorage,观察其反馈,能够发现localStorage的value能够正确传入对象内容 对JSON序列化的对象进行typeof类型判断,也返回string结果,印证我们的想法
// 将obj转成JSON格式的字符串
const infoString = JSON.stringify(info)
console.log(infoString);
//{"name":"coderwhy","age":18,"friends":{"name":"XiaoYu"},"hobbies":["篮球","足球"]}
console.log(typeof infoString);//string
// 将对象数据存储localStorage
localStorage.setItem("info", infoString)
图32-3 JSON序列化后的存储结果
JSON序列化后,相当于披上一层马甲,我们是没办法对一个字符串进行各种常规读取属性操作的,因此需要脱下马甲 JSON.parse
这个静态方法就是专门做这件事情的,parse可以翻译为解析,与功能一致,是用来解析 JSON 字符串的
//text:要被解析成 JavaScript 值的字符串
JSON.parse(text, reviver)
因此 穿上马甲
和脱下马甲
对应了两个专业名词:JSON序列化
和JSON解析
,这两套组合拳通常是结合使用才能发挥最大威力,值得了解一下
// 将obj转成JSON格式的字符串
const infoString = JSON.stringify(info)
console.log(typeof infoString);//string
//将JSON格式字符串转回obj
const infoParse = JSON.parse(infoString)
console.log(typeof infoParse);//object
1.2.3 stringify序列化细节
在学习JSON这两个静态方法的语法时,有看到stringify静态方法的第二个参数 replacer
,那该参数是做什么的?为什么序列化需要这个参数呢?它解决了什么痛点问题?replacer参数
是可选的,作用是筛选过滤所需部分当一个对象想转为JSON序列化,又不希望全部转化时,就可以采用replacer参数,以 数组形式
传入希望保留的属性即可,这种最常见的用法
const info = {
name: "coderwhy",
age: 18,
friends: {
name: "XiaoYu"
},
hobbies: ["篮球", "足球"]
}
//replacer参数为数组
const infoString = JSON.stringify(info,['name','age'])
console.log(infoString);//{"name":"coderwhy","age":18}
除此之外,还允许传入回调函数或者null,默认则认定为未提供 传入回调函数则有两个参数,分别为key和value,也就是键值对的遍历,我们可以在回调函数中拿到即将转为JSON字符串数据的每一个键值对进行处理,我们可以简单理解为一个拦截器作用 若 replacer参数
为null或者未填,则为全部序列化
// 将obj转成JSON格式的字符串
const infoString = JSON.stringify(info, (key, value) => {
if (key === 'name') value = '小余'
return value
})
console.log(infoString);//{"name":"小余","age":18,"friends":{"name":"小余"},"hobbies":["篮球","足球"]}
MDN文档的解释更为精准:如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化 我们能够发现,这些操作本可以声明一个对象变量进行调整修改再传入stringify静态方法中,而 replacer参数
做到结合效果,说明需要进行拦截修改的操作频率非常高直接在序列化的过程中进行拦截,而不需要更改原来的数据结构。这种无副作用的操作方式更符合函数式编程的理念,即通过纯函数来避免修改原始数据 减少一次对对象的额外操作,逻辑简化在replacer参数中,从而提高效率,并且传入改变的对象依旧是原始对象,不存在中转对象的干扰,阅读线路更清晰明确 统一处理,在代码结构也更为清晰,stringify方法所负责的对象将一直保持为原始对象,而无需考虑需要对原始对象负责还是对新声明对象变量负责
图32-4 replacer参数的拦截
stringify方法还存在第三个参数 space
,该参数接收string或者number两种类型,指定缩进用的空白字符串,用于美化输出(pretty-print)传递string类型,则字符串被视为空格填写内容 传递number类型,则指定空格数量 但不管哪种方式,缩进上限都为10,字符串内容如果超出10位就选择前10位
const infoString = JSON.stringify(info, null,2)//缩进为2
MDN文档解释: space
参数用来控制结果字符串里面的间距。如果是一个数字,则在字符串化时每一级别会比上一级别缩进多这个数字值的空格(最多 10 个空格);如果是一个字符串,则每一级别会比上一级别多缩进该字符串(或该字符串的前 10 个字符)
图32-5 space缩进效果展示
若一个被序列化的对象拥有 toJSON
方法,会直接将toJSON的返回值作为结果,这一点和Promise的thenable很相似该 toJSON
方法就会覆盖该对象默认的序列化行为:不是该对象被序列化,而是调用toJSON
方法后的返回值会被序列化
const info = {
name: "coderwhy",
age: 18,
friends: {
name: "XiaoYu"
},
hobbies: ["篮球", "足球"],
toJSON: function () {
return "toJSON已经设置"
}
}
const infoString = JSON.stringify(info)
console.log(infoString);//"toJSON已经设置"
1.2.4 字符串解析parse细节
parse静态方法除了参数1能够用来传递解析,还有第二参数reviver,而这又有什么作用? reviver
的单词含义来源于 “revive”,直接翻译是复活、恢复。在这里需要结合语境去考虑,可以翻译为"还原回调函数",该"还原回调函数"作用与stringify方法中的replacer参数
作用一致,回调参数也一致(都为key、value)不同之处在于reviver参数是作用于解析阶段,在将JSON序列化内容还原后,返回数据前进行拦截,拦截后则可以进行处理 因此我们能够认为作用都是拦截,但 作用时机不同
(序列化与阶段两个不同阶段),作用的顺序也不同
,replacer参数是在序列化前拦截,reviver参数是在还原解析后,返回内容前进行拦截
图32-6 JSON序列化与解析流程
const JSONString = '{"name":"coderwhy","age":19,"friends":{"name":"小余"},"hobbies":["篮球","足球"]}'
const info = JSON.parse(JSONString, (key, value) => {
if (key === "age") {
return value - 1
}
return value
})
console.log(info)
// {
// name: 'coderwhy',
// age: 18,
// friends: { name: '小余' },
// hobbies: [ '篮球', '足球' ]
// }
MDN文档解释reviver参数:如果指定了 reviver
函数,则解析出的 JavaScript 值(解析值)会经过一次转换后才将被最终返回(返回值)。更具体点讲就是:解析值本身以及它所包含的所有属性,会按照一定的顺序(从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身)分别的去调用reviver
函数,在调用过程中,当前属性所属的对象会作为this
值,当前属性名和属性值会分别作为第一个和第二个参数传入reviver
中。如果reviver
返回undefined
,则当前属性会从所属对象中删除,如果返回了其他值,则返回的值会成为当前属性新的属性值。
1.2.5 序列化深拷贝
JSON 序列化深拷贝是一种利用 JSON 的
序列化(serialization)
和反序列化(deserialization)
来实现对象深拷贝的技术手段。这种方法能够将一个对象的所有层次的数据完全复制出来,从而生成一个与原始对象没有引用关系的全新对象在前面的学习中,我们打下坚实的基础,我们能够理解在JS中是存在
引用赋值
的情况,当两个变量共享同一个引用地址时,对其一的改变会同时影响其二,有时候我们并不希望产生这种藕断丝连的关系简单回顾案例,创建obj对象,将obj赋值给新变量info,对info进行修改,打印obj对象 obj对象被改变,因为obj赋值info时,是赋值其堆内存地址(类似0xa00格式) 只有存放与栈内存的基础类型数据不需要担心被改变
const obj = {
name:"小余",
age:18
}
const info = obj
info.name = "coderwhy"
console.log(obj);//{ name: 'coderwhy', age: 18 }
在这里简单回顾深浅拷贝概念:
深拷贝(Deep Copy)是指将一个对象中的所有数据,包括嵌套的对象和数组,都完全复制出来,生成一个新的对象,它与原来的对象在内存中是完全独立的 与之相对的浅拷贝(Shallow Copy)只会复制对象的第一层引用,而对嵌套对象或数组的引用不会进行深层次的复制,结果是拷贝出来的对象和原对象共享嵌套对象或数组
新变量info使用对象字面量的方式创建了一个对象,将原有对象内部的数据搬迁进去,从内存角度来说,这已经指向两处不同的内存空间,但若搬迁对象内部依旧还有引用情况,则深处依旧指向同一内容
图32-7 浅拷贝示意图
const obj = {
name:"小余",
names:{
name1:'coderwhy1',
name2:'coderwhy2',
}
}
//浅拷贝 改变了第一层的内存存储地址
const info = {...obj}
info.name = '小余007'//位于第一层,info内存空间与obj不同,不会影响obj
info.names.name1 = 'coderwhy123'//位于第二层,info的names与obj的names相同,会影响obj的names属性
console.log(obj);//{ name: '小余', names: { name1: 'coderwhy123', name2: 'coderwhy2' } }
而利用JSON中的序列化与解析两步骤是能够实现深拷贝的,之所以可以实现,则需要回顾前面特地给出的MDN文档对reviver参数的解释:从最里层向外一层层调用reviver参数 这说明我们能够拿到对象中的每一个基础数据类型,去除所有引用情况,实现所有深层内容的遍历并拷贝 但JSON深拷贝并不是万能的,虽简单易用,但JSON序列化是无法对函数进行处理,默认会进行移除,还有无法处理循环引用、不支持特殊对象、性能不佳(不适合大型对象)等问题 对于该缺陷我们可以在stringify方法的replacer参数中进行拦截单独处理,但终究需要考虑其缺陷与需求对冲问题 我们后续会讲解如何编写深拷贝的工具函数,那么这样就可以对函数的拷贝进行处理了 而JSON深拷贝在该过程分为两步骤是密不可分的,也被称为序列化和反序列化(解析): 序列化:将对象转换为 JSON 字符串,这是一个深层次的数据提取过程,确保对象的所有属性(包括嵌套的对象和数组)都被提取 反序列化:将生成的 JSON 字符串转换为一个新的 JavaScript 对象,得到的对象是原始对象的深层拷贝
//先序列化后解析
const info = JSON.parse(JSON.stringify(obj))
info.name = '小余007'
info.names.name1 = 'coderwhy123'
console.log(obj);//{ name: '小余', names: { name1: 'coderwhy1', name2: 'coderwhy2' } }
尽管如此,在大多数需要对普通数据对象进行深拷贝的情况下,JSON 序列化深拷贝依然是非常有效的方法,适用于处理没有复杂结构的对象
二、初识Storage
Storage
是浏览器提供的Web 存储(Web Storage) API,用于在用户浏览器中保存数据。它为 Web 应用提供了一种简单、高效的数据持久化方案,允许开发者以键-值对的形式在客户端存储数据,且数据不会随页面刷新而丢失
2.1 localStorage和sessionStorage的区别
WebStorage主要提供了一种机制,可以让浏览器提供一种比cookie(后续学习)更直观的key、value存储方式,其中Storage 包括两种主要类型:
localStorage:本地存储,提供的是一种永久性的存储方法,即使关闭浏览器或重新启动计算机,数据依然存在,除非手动删除
sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除
图32-8 Storage存储对应浏览器位置
由于两种类型的特点不同,导致了应用场景的不一致,也是两者最大的区别所在 在用户登录时,可以使用 localStorage
存储用户 token 或身份信息(需注意安全性),方便在页面中进行简单的认证操作,这样用户就不必每次上线都需要重新登录而在用户填写长表单或进行某些操作时,可以将中间状态保存在 sessionStorage
中,避免刷新页面导致的数据丢失。当然我们可能会想,没事的情况怎么会故意去刷新,这里的刷新涵盖含义较为广泛,不仅指Ctrl+R的页面刷新,也包括了页面跳转再返回,表单关闭等各种业务情况,因此使用范围也很大,在学习项目时,能够更深刻理解这点以上是从应用的角度出发,因此如何界定使用方式,更应该从数据在实际应用的生命周期进行判断 这里提到一个概念,数据的生命周期,两个类型的周期都是如何体现的,这则需要深入浏览器原理 localStorage
中的数据被写入到浏览器缓存数据库(通常是基于文件的存储),并且持久保存在设备的硬盘中。浏览器会将这些数据存储在其特定的存储目录中,因此即使用户关闭浏览器,数据也不会丢失而 sessionStorage
中的数据是与浏览器标签页或者窗口实例相关联的。浏览器为每个打开的标签页(或窗口)分配一个独立的 session context,并在该上下文中存储数据。这些数据不会被持久化到硬盘,而是保存在内存中或类似于内存的临时存储中,因此它们的生命周期通常限于当前浏览器标签页的打开状态由于存储位置的不同,导致其数据的使用范围(作用域)也不同 localStorage
数据位于磁盘,可以通过浏览器的内置存储管理来实现同一个源下的所有页面中保持一致的数据,所以是跨会话、跨标签页的,如果多个标签页属于同一个域名,它们可以读取并共享相同的localStorage
数据。这对于在同一个应用中不同页面之间共享一些用户信息非常有用,例如认证 token 等(例如同时打开多个掘金都是登录状态)sessionStorage
数据的作用域仅限于当前标签页或窗口,同一网站在不同的标签页中拥有各自独立的sessionStorage
实例,彼此之间的数据无法共享。每个打开的标签页(或窗口)都有一个单独的sessionStorage
实例。但从当前标签页打开的其他页面,会被视为同一会话的延伸(除非特地设置打开的是一个全新空白页),这并不与会话存储的特点冲突而且由于我们在pnpm中,对操作系统已经有一定的了解,知道磁盘与内存读写速度是有差距的,因此还可以从性能角度去看待区别问题 localStorage
的数据会持久化到磁盘中,读写时相对较慢,尤其是当存储的数据量较大时,磁盘 I/O 的开销会影响性能sessionStorage
的数据存储在内存中,读取速度更快,适合在页面短期内频繁访问数据的场景其中最需要记住的是以下三点表现:
关闭网页后重新打开,localStorage会保留,而sessionStorage会被删除 在页面内实现跳转,localStorage会保留,sessionStorage也会保留 在页面外实现跳转(打开新的网页),localStorage会保留,sessionStorage不会被保留
2.2 Storage常见方法与属性
Storage作为Web Storage API系列,一共有五个实例方法和一个实例属性,我们都会进行使用上的学习
首先是存储和读取的经典方法:
setItem
和getItem
内存存储的数据结构为键值对,因此使用方式将围绕该方式展开,setItem传入所需键值对,getItem传入所需键获取所需值,在这点上和JS对象的使用方式相似
//keyName:要创建的键
//keyValue:要创建的值(必须为字符串)
setItem(keyName, keyValue)
//keyName:传入键,返回需要的值
getItem(keyName)
Storage还提供类似数组的使用方式:key方法 该方法接受一个数值n作为参数,返回存储中的第n个key名称 我们可以理解为有一个数组,有序存储了所有的key值,可以以索引获取对应的key值(索引以0为起点)
// 1.setItem
localStorage.setItem("name", "coderwhy")
localStorage.setItem("age", 18)
// 2.key方法
console.log(localStorage.key(0))//name
在key方法的基础上,我们延伸出对Storage属性length的学习 key方法作为一个数组形式的使用,我们明确知道索引的起点,也知道索引的有序规律(0开始,每次加1),但我们不知道这个数组有多长,无法精准遍历出所有的key值 因此Storage提供了唯一的一个属性length,使用方式与数组相同,配合key方法能够将所有的key值遍历出来,并通过判断操作筛选出所需的key值进行针对性操作
localStorage.setItem("name", "coderwhy")
localStorage.setItem("age", 18)
console.log(localStorage.length)
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
//获取所有key中对应的值
console.log(localStorage.getItem(key))
}
除此之外,Storage还有两个删除方法:removeItem和clear,区别在于前者精准清除,后者全部删除 removeItem方法接受一个key值参数,以便实现定点删除 而clear作为无差别全删除并不需要参数,直接调用即可
//针对性删除name
localStorage.removeItem("name", "coderwhy")
//全部删除
localStorage.clear()
以上进行演示时,我们采用localStorage(本地存储)进行操作,但这些做法同样存在sessionStorage,具备的五个方法和一个属性都是一模一样的,包括使用方式和效果,这里则不再演示
2.3 封装Storage
在实际应用中,我们需求往往较为复杂,此时原生的Storage方法无法满足我们的需求,就可以利用原生方法封装一个关于Storage的定制化工具类,实现更为强大的效果,例如深拷贝就可以两个原生方法封装为一个方法使用,更为高效便捷 想法产生,开始实践,首先需要搭建一个基础的工具类模板
class HYCache {
constructor() {
}
}
const localCache = new HYCache()
const sessionCache = new HYCache()
//统一导出
export {
localCache,
sessionCache
}
在完成框架搭建后,可以来实现具体的逻辑,在这里需要进一步细分,如何区分本地存储和会话存储,毕竟都是使用同一个类 由于只有两个类型,非常适合布尔值,因此我们采用该方法来判断,默认true为本地存储,第一参数传入false为会话存储。然后根据用户传入布尔值结果,来选择逻辑实现,工具本身不需要操心需要选择哪一个类型
class HYCache {
constructor(isLocal = true) {
// 由实例传入参数决定本地存储 or 会话存储
this.storage = isLocal ? localStorage : sessionCache
}
}
const localCache = new HYCache()
const sessionCache = new HYCache(false)
Storage存储传入的数据必须是一个字符串,在使用原生方法时,需要时刻记住这点,提前转为JSON字符串,在这里可以进行封装,不管是序列化还是解析的过程,都直接封装起来,用户使用工具时,可以直接拿到转化后的结果,剩下内容按部就班即可,有需求再根据需求具体调整,这样就是一个扩展性很好的工具类
class HYCache {
constructor(isLocal = true) {
this.storage = isLocal ? localStorage: sessionStorage
}
setItem(key, value) {
if (value) {
this.storage.setItem(key, JSON.stringify(value))
}
}
getItem(key) {
let value = this.storage.getItem(key)
if (value) {
value = JSON.parse(value)
return value
}
}
removeItem(key) {
this.storage.removeItem(key)
}
clear() {
this.storage.clear()
}
key(index) {
return this.storage.key(index)
}
length() {
return this.storage.length
}
}
三、初识IndexedDB
什么是IndexedDB呢?我们能看到DB这个词,就说明它其实是一种数据库(Database),通常情况下在服务器端比较常见 在实际的开发中,用于浏览器的情况非常少见且不太合适,因为缓存内容会非常多导致浏览器负荷非常大,产生性能上的问题,大量的数据都是存储在数据库的,客户端主要是请求这些数据并且展示 而IndexedDB的浏览器提供的一种低级别的客户端数据库,是一个非关系型(NoSQL)数据库,比 Web Storage( localStorage
和sessionStorage
)更为强大有时候我们可能会存储一些简单的数据到本地(浏览器中),比如token、用户名、密码、用户信息等,比较少存储大量的数据,那么如果确实有大量的数据需要存储,这个时候可以选择使用IndexedDB,因为Storage存储上限为5-10MB,而IndexedDB能够达到上百MB IndexedDB 是一个浏览器提供的底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs)) 也被称为是事务型数据库系统,类似于基于 SQL 的 RDBMS。然而,不像 RDBMS 使用固定列表,IndexedDB 是一个基于 JavaScript 的面向对象数据库 在 IndexedDB 中,事务是指用于管理一组数据库操作的逻辑单元,确保这些操作要么 全部成功
,要么全部不执行
举个简单的例子,转账操作,当转给他人100块钱时,我们的钱包余额需要同步减去100,必须同时发生,不能因为转账过程的某个步骤出了问题,导致我钱扣了然后没转过去,也不能转过去了然后钱包没扣,这种逻辑是必须保持连贯的,连贯的一个逻辑就被称为一个单元,出现问题就会整个单元进行回滚操作,这个概念非常重要,但这些更多是后端方面的知识,这里作为简单了解即可 IndexedDB本身就是基于事务的,我们只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务即可 有很多的数据,为什么不直接存储在一个本地文件里,然后直接读取就好,而是专门需要数据库来进行,看似好像多了一个环节更"麻烦"了 但其实这是更简单了,因为任何内容在面对基数越庞大的问题上,对性能的要求就越高,因为每一点的性能浪费在庞大基数的基础上,都会带来非常明显的感受 数据库拥有各种数据结构,都是为了对数据操作性能上的极致优化,做到效率上的提升
3.1 IndexedDB连接数据库
数据库中所有的操作都来自最基础的增删改查,在这里我们也主要演练这四种操作 不过在开始操作之前,我们需要先连接上数据库。连接数据库后,才能对数据库中的数据进行操作 为了获取数据库的访问权限,需要在 window 对象的 indexedDB 属性上调用 open()实例方法。该方法返回一个 IDBRequest
对象
//name:数据库名称
//version:指定数据库版本
open(name, version)
此时可能会有一个疑惑,我们不是要创建数据库吗?怎么看open方法是打开数据库呢?和目的不太一致 open方法同时兼顾创建和打开数据库的功能,如果当前没有数据库则创建,如果有数据库则打开
//创建数据库,库名coderwhy
const dbRequest = indexedDB.open("coderwhy", 3)
3.2 IndexedDB数据库操作
在完成打开数据库后,通过返回的实例进行操作 伴随着创建数据库后,主要的数据库操作时机包括:打开失败(onerror)、打开成功(onsuccess)、第一次打开或者数据库版本升级时(onupgradeneeded),每一个操作都是回调函数 回调函数里的回调参数是一个event,通过获取 event.target.result
可以对数据库开始进行操作同时这里补充一个概念:一个软件中允许存在多个数据库,一个数据库允许存在多张表,在IndexedDB中,数据库内存在的不是表,而是存储对象,但性质是差不多的 通常创建数据库后,第一件事情就是创建存储对象(表),参数1是对象存储名称,参数2是一个对象,主要有两个配置项 keyPath
和autoIncrement
对象存储的名称用于唯一标识对象存储,可以理解为数据库中的表名 keyPath
是主键,理解为一个唯一ID。**autoIncrement
**则是一个布尔值,表示是否在存储数据时自动生成主键。如果设置为true
,则每次添加数据时,数据库会自动生成一个递增的主键值
dbRequest.onerror = function(err) {
console.log("打开数据库失败~")
}
//全局存储db,共享使用
let db = null
dbRequest.onsuccess = function(event) {
db = event.target.result
}
// 第一次打开/或者版本发生升级
dbRequest.onupgradeneeded = function(event) {
const db = event.target.result
console.log(db)
// 创建一些存储对象
db.createObjectStore("users", { keyPath: "id" })
}
在浏览器中,可以看到IndexedDB中已经显示我们创建的数据库了,包括其中的配置
图32-9 IndexedDB数据库信息
我们接下来想对数据库进行操作,则需要交互按钮,在index.html中创建四个按钮,获取DOM元素,将其绑定到JS点击事件中即可
//index.html
<button>新增</button>
<button>查询</button>
<button>删除</button>
<button>修改</button>
// 获取btns, 监听点击
const btns = document.querySelectorAll("button")
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function() {
switch(i) {
case 0:
console.log("点击了新增")
case 1:
console.log("点击了查询")
case 2:
console.log("点击了删除")
case 3:
console.log("点击了修改")
}
}
}
我们将每一次操作,都归纳为一个事务对象,作为数据库的一次操作,保证数据库的一致性,而 db.transaction()
方法用于在数据库中创建一个事务参数 "users"
指定对哪一个或哪几个对象存储(object store)进行操作。在这里,"users"
是对象存储的名称,表示要对"users"
对象存储进行操作,也可以使用数组形式来一次性创建多个事务参数 "readwrite"
表示事务的类型,这里是"readwrite"
,意味着事务允许对对象存储中的数据进行读写操作。还有另外一种常用的事务模式是"readonly"
,表示只对数据进行读取,不允许写入返回值transaction则是一个事务对象 创建明确的事务后, transaction.objectStore("users")
通过事务获取对象存储,即对数据库中的"users"
对象存储进行操作,返回的store
是对象存储的引用,之后可以通过这个引用来进行数据的增、删、改、查等操作
const transaction = db.transaction("users", "readwrite")
console.log(transaction)
const store = transaction.objectStore("users")
想要对数据库进行操作的所有前置要求均已完成,接下来对数据进行操作前,先创建一点数据
class User {
constructor(id, name, age) {
this.id = id
this.name = name
this.age = age
}
}
const users = [
new User(100, "why", 18),
new User(101, "coderwhy", 40),
new User(102, "小余", 30),
]
对DOM进行监听,增删改查四个按钮绑定的索引分别为0至3,也对应了watch循环中的4个阶段 我们在0中增添数据,1中删除数据、2中修改数据、3中查询数据 返回的store通过add方法进行增添数据,每一次增添都设置成功的回调来提示我们数据插入成功,第一次事务操作完成时,通过oncomplete来提示我们已经全部添加完成
case 0:
console.log("点击了新增")
for (const user of users) {
const request = store.add(user)
request.onsuccess = function() {
console.log(`${user.name}插入成功`)
}
}
transaction.oncomplete = function() {
console.log("添加操作全部完成")
}
break
图32-10 IndexedDB数据库添加内容展示
查询有两种方式: 方式一根据主键查询,适用于知道具体的主键值( keyPath
)的情况,简单直接且高效方式二使用游标遍历查询,可以用于实现复杂的过滤逻辑,但效率较低 方式一直接使用get方法非常高效,因为它直接通过主键进行定位,相当于传统数据库中通过索引查询,找到目标后立即返回 方式二通过 store.openCursor()
打开一个游标,遍历对象存储中的每一条记录,通过逻辑条件筛选数据,适合查找满足特定条件的数据游标(Cursor) 是 IndexedDB 中用于遍历对象存储(object store)或索引(index)的一个工具,它相当于一个指针,指向对象存储中的每一个记录,允许我们逐条访问、读取或操作数据 指针是C语言中的难点概念,我们可以理解为在对象存储中存在着许多小块,每一块都是一个记录,指针指向首块记录cursor.key与cursor.value指向对象的键值对,然后通过 cursor.continue()方法
移动该指针
case 1:
console.log("点击了查询")
// 1.查询方式一(知道主键, 根据主键查询)
// const request = store.get(102)
// request.onsuccess = function(event) {
// console.log(event.target.result)
// }
// 2.查询方式二:
const request = store.openCursor()//打开一个游标
request.onsuccess = function(event) {
const cursor = event.target.result
if (cursor) {
if (cursor.key === 101) {
console.log(cursor.key, cursor.value)
} else {
cursor.continue()
}
} else {
console.log("查询完成")
}
}
break
需要注意的是游标查询的效率较低,因为它需要逐条遍历所有记录,尤其当对象存储包含大量数据时,这种方式的性能会受到影响,更适合需要筛选特定条件内容,对于简单的查找而言,get方法是首选
对应删除和修改来说,也都是利用游标来进行操作,毕竟拿到了具体的每一条数据,想要进行针对性(批量)修改或者删除都是非常简单,游标在遍历到每一个属性的基础上,提供了对应修改数据和删除数据的方法,分别为:
cursor.update(value)
和cursor.delete()
//删除
case 2:
console.log("点击了删除")
const deleteRequest = store.openCursor()
deleteRequest.onsuccess = function(event) {
const cursor = event.target.result
if (cursor) {
if (cursor.key === 101) {
cursor.delete()
} else {
cursor.continue()
}
} else {
console.log("查询完成")
}
}
break
//修改
case 3:
console.log("点击了修改")
const updateRequest = store.openCursor()
updateRequest.onsuccess = function(event) {
const cursor = event.target.result
if (cursor) {
if (cursor.key === 101) {
const value = cursor.value;
value.name = "curry"
cursor.update(value)
} else {
cursor.continue()
}
} else {
console.log("查询完成")
}
}
break
}
对于IndexedDB作一个了解到目前就足够了,感兴趣的可以从MDN文档中了解更多细节,完整操作代码如下:
const dbRequest = indexedDB.open("coderwhy", 3)
dbRequest.onerror = function (err) {
console.log("打开数据库失败~")
}
//全局存储db,共享使用
let db = null
dbRequest.onsuccess = function (event) {
db = event.target.result
}
// 第一次打开/或者版本发生升级
dbRequest.onupgradeneeded = function (event) {
const db = event.target.result
console.log(db)
// 创建一些存储对象
db.createObjectStore("users", { keyPath: "id" })
}
class User {
constructor(id, name, age) {
this.id = id
this.name = name
this.age = age
}
}
const users = [
new User(100, "why", 18),
new User(101, "coderwhy", 40),
new User(102, "小余", 30),
]
// 获取btns, 监听点击
const btns = document.querySelectorAll("button")
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
const transaction = db.transaction("users", "readwrite")
console.log(transaction)
const store = transaction.objectStore("users")
switch (i) {
case 0:
console.log("点击了新增")
for (const user of users) {
const request = store.add(user)
request.onsuccess = function () {
console.log(`${user.name}插入成功`)
}
}
transaction.oncomplete = function () {
console.log("添加操作全部完成")
}
break
case 1:
console.log("点击了查询")
// 1.查询方式一(知道主键, 根据主键查询)
// const request = store.get(102)
// request.onsuccess = function(event) {
// console.log(event.target.result)
// }
// 2.查询方式二:
const request = store.openCursor()//打开一个游标
request.onsuccess = function (event) {
const cursor = event.target.result
if (cursor) {
if (cursor.key === 101) {
console.log(cursor.key, cursor.value)
} else {
cursor.continue()
}
} else {
console.log("查询完成")
}
}
break
//删除
case 2:
console.log("点击了删除")
const deleteRequest = store.openCursor()
deleteRequest.onsuccess = function (event) {
const cursor = event.target.result
if (cursor) {
if (cursor.key === 101) {
cursor.delete()
} else {
cursor.continue()
}
} else {
console.log("查询完成")
}
}
break
//修改
case 3:
console.log("点击了修改")
const updateRequest = store.openCursor()
updateRequest.onsuccess = function (event) {
const cursor = event.target.result
if (cursor) {
if (cursor.key === 101) {
const value = cursor.value;
value.name = "curry"
cursor.update(value)
} else {
cursor.continue()
}
} else {
console.log("查询完成")
}
}
break
}
}
四、初识Cookie
Cookie(复数形态Cookies),又称为“小甜饼”。类型为“小型文本文件",某些网站为了辨别用户身份而存储在用户本地终端(Client Side)上的数据
浏览器会在特定的情况下携带上cookie来发送请求,我们可以通过cookie来获取一些信息 Cookie 是由服务器生成的,并存储在浏览器中,浏览器会将 Cookie 作为请求头的一部分发送给服务器,从而在客户端和服务器之间共享数据 通常用来验证身份,当我们首次登录账号时,将用户名与密码传给服务器,服务器校验正确后,会返回一系列数据,在返回数据时会连带着Cookie一起返回 当做出一些需要登录账号才能做的操作时,在将操作请求发送服务器时,cookie也会从浏览器中一起被携带过去,作为服务器判断当前是否登录的凭证,登录则返回数据进行操作,否则拒绝 Cookie总是保存在客户端中,按在客户端中的存储位置,Cookie可以分为内存Cookie和硬盘Cookie 内存Cookie由浏览器维护,保存在内存中,浏览器关闭时Cookie就会消失,其存在时间是短暂的 硬盘Cookie保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理 这个概念与前面Storage的两种类型相似 那如何判断一个cookie是内存cookie还是硬盘cookie呢? 没有设置过期时间,默认情况下cookie是内存cookie,在关闭浏览器时会自动删除 有设置过期时间,并且过期时间不为0或者负数的cookie,是硬盘cookie,需要手动或者到期时,才会删除。这个过期时间是由服务器设置的,但有一致命点需要注意,一旦服务器设置并且返回客户端后,将无法对该过期时间进行任何操作,主动权从服务器交到用户手中 如下图中的set-Cookie中就存在内容以及对应的过期时间
图32-11 Cookie显示图
常见设置Cookie都在后端进行操作,前端更多作为接收方,在这里以Node的Koa框架进行举例 第一次浏览器发起请求,将用户名与密码传递给服务器,服务器接收后,返回登录信息和通过 HTTP 响应头 Set-Cookie
向客户端发送一个或多个 Cookie,浏览器接收到Set-Cookie
后,会存储这些 Cookie,并在后续的请求中将它们自动添加到请求头中,发送给服务器第二次浏览器发起请求,携带信息和对应Cookie到服务器中进行验证,服务器根据Cookie判断当前用户是否登录,是哪一个用户等信息,再决定返回什么内容给浏览器 Cookie过期后,则由默认值undefined替代原有内容
图32-12 服务器与浏览器的Cookie交互
4.1 cookie常见属性
cookie存在对应的生命周期(生效阶段):
会话 Cookie(Session Cookie):默认情况下,Cookie 是会话级别的,浏览器关闭后,Cookie 会自动失效 持久 Cookie(Persistent Cookie):如果设置了 expires
或max-age
属性,则 Cookie 可以被持久保存,即使关闭浏览器,直到到达指定的过期时间后才失效expires:设置的是Date.toUTCString(),设置格式是;expires=date-in-GMTString-format max-age:设置过期的秒钟,max-age=max-age-in-seconds (例如一年为 60*60*24*365
)Cookie 与特定的域名和路径相关联,因此它们只能被同一域名下的页面访问,确保数据的安全性
这说明Cookie是存在对应作用域范围的,也是做到当前页面发起请求,"其他"页面也能共享对应Cookie的原因,这都是基于同一域名下的前提条件才能实现 在这一点上,Cookie与Storage存储有一定的相似性,但Cookie还可以主动设置哪些主机可以接受cookie Domain:指定哪些主机可以接受cookie
如果不指定,那么默认是 origin,不包括子域名 如果指定Domain,则包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org),developer是子域名 Path:指定主机下哪些路径可以接受cookie
例如,设置 Path=/docs,则以下地址都会匹配: /docs
、/docs/Web/
、/docs/Web/HTTP
通过浏览器左上角的 i符号
或者DevTools中Application的Cookies选项(位于Storage列表中),能够查看对应的cookie
图32-13 浏览器查看Cookie位置
4.2 客户端设置cookie
但能够通过这两种方式来找到对应Cookie选择进行删除的,往往都是程序员群体 对于普通用户而言,这两种做法都不太方便,也很难想到,因此各类网站往往会提供一个清除Cookie的按钮,通常这个按钮就叫做 退出登录
,当点击退出登录时,相当于删除了Cookie那我们要如何通过代码来删除Cookie?
通过window中的document.cookie是拿不到对应的cookie的,但我们可以通过这里设置Cookie(不能获取原来服务器的Cookie) 通过 document.cookie = ''
掷为空是删不掉Cookie的,正确的操作是document.cookie="name='coderwhy';max-age=0"
,首先选择我们所想要删除的内容,然后将其过期时间设置为0,相当于马上过期,则值就会默认转为undefined,实现"删除"效果
//js直接设置和获取cookie
console.log(document.cookie);
//这个cookie会在会话关闭时被删除掉
document.cookie = "name=coderwhy"
document.cookie = "age=18"
//设置cookie,同时设置过期时间(默认单位是秒钟)
document.cookie = "name=coderwhy;max-age=10"
但目前,使用Cookie的情况越来越少了,因为Cookie会附加到每一次http请求中(浏览器自动携带),哪怕我们不需要这个Cookie也是,会浪费用户一定的流量 Cookie是明文传输的,哪怕是加密后的内容(类似md5)再"加盐",也有被破解的安全风险,这种能够避免应该尽可能避免 Cookie有大小限制4KB 验证登录时如果通过Cookie,则会产生很强的依赖性,必须依赖Cookie来确定登录,但我们 客户端不止浏览器,还有IOS、Android、小程序等等,有些客户端有可能是没办法设置Cookie或者需要手动添加Cookie的 Cookie随着时代的发展只会使用得越来越少,现在流行的做法是使用token,等到做项目时,就会学习到这方面相关的知识点了