介绍
本文是 JavaScript 高级深入浅出的第 22 篇,介绍了前端会使用到的浏览器本地储存
正文
1. JSON
为什么要介绍 JSON ?因为 JSON 是一种非常重要的数据格式,它并不是编程语言,而是一种可以在服务器端与客户端相互传输的数据格式
JSON 的全称叫做:JavaScript Object Notation JavaScript 对象符号
- JSON 是由 Douglas Crockford 构想和设计的一种轻量级资料交换格式,算是 JS 的一个子集
- 虽然 JSON 被提出来时主要应用于 JS 中,但是现在已经独立于编程语言。
- 很多编程语言都实现了 JSON 转为对应模型的方式
其他的传输格式:
- XML:在早期的网络传输时通过 XML 来进行数据交换的,但是这种格式在解析、传输等方面均不如 JSON,下载使用的已经越来越少了
- Protobuf:另一个在网络传输中目前使用的越来越多的格式时 Protobuf,但是直到 2021 年的 3.x 版本才支持 JS,所以目前在前端领域用得还不是很广泛
目前 JSON 的应用领域:
- 网络传输 JSON 数据
- 项目的某些配置文件
- 非关系型数据库(NoSQL)将 JSON 作为存储格式
1.1 JSON 基本语法
JSON 由 .json 作为后缀,JSON 中没有注释,如果想写注释,后缀改成 .jsonc(JSON With Comments),JSON 的最后一个成员不能加 ,
JSON 的顶层支持三种类型的值:
- 原始值:number、string(必须是双引号)、boolean、null
- 键值对:key-value 组成,key 必须是双引号的字符串,value 可以是原始值和键值对
- 数组值:数组的值可以是原始值、键值对和数组值
{
"name": "foo"
}
注意:顶层可以是一个数组
[
{
"name": "foo1"
},
"bar",
123
]
1.2 JSON 序列化
某些情况我们可能需要将 JS 中的对象数据转换为 JSON,这样方便储存。
-
比如我们希望将一个对象保存在 localStorage 中
-
由于 localStorage 仅支持 key-value,同时 value 只能是字符串,所以直接保存一个对象,就相当于
Object.toString(),结果就变成了[object Object] -
通过
JSON.stringify(obj)将一个对象序列化成 JSON -
通过
JSON.parse(jsonString)将一个 JSON 解析为 Object,并返回
JSON.stringify
可以通过 JSON.stringify() 来将一个 Object 转换为 JSON,但其实这个方法还可以传入第二个和第三个参数:
JSON.stringify(value[, replacer [, space]])
- value:要序列化的值
- replacer
- 是一个函数,那么序列化中的每一项都要经过这个函数处理
- 是一个数组,只有包含这个数组中的属性名才会被序列化
- 是 null 或未提供,所有的属性都会被序列化
- space:指定缩进用的空白字符串
- 是一个数字,代表有多少空格,最大是 10,小于 1,则没有空格
- 是一个字符串,那么字符串被作为空格(超过 10 位只取 10 位)
- 是 null 或未提供,则没有空格
JSON.parse
JSON.parse(text[, reviver])
- text:要解析为 JS 的值的 JSON 字符串
- reviver:转换器,如果传入该参数(函数),可用来修改解析生成的原始值,调用时机在 parse 转换之前
利用 JSON 序列化做深拷贝
一种简单深拷贝的方法是将一个对象通过序列化再转换出来:
const obj = {
name: 'alex',
foo: 'bar'
}
const deepCloneValue = JSON.parse(JSON.stringify(obj))
console.log(obj === deepCloneValue) // false
但是有弊端,由于 JSON 并不支持 Function,因此 Function 无法通过这种方式进行深拷贝,但是如果只是简单的值,就可以通过这种方式来进行深拷贝
2. 浏览器存储
2.1 storage
WebStorage 主要提供了一种机制,可以让浏览器提供一种比 cookie 更加直观的 key-value 储存方式,在 Chrome 中,storage 最大储存为 5MB。
- localStorage:本地存储,提供一种永久性的储存方法
- sessionStorage:会话存储,提供的是本次会话的存储
区别在哪?
- 关闭网页重新打开,localStorage 存在,sessionStorage 不存在
- 页面内实现跳转,localStorage 和 sessionStorage 存在
- 在页面内实现跳转(以新的标签页作为跳转),localStorage 有,sessionStroage 没有
storage 的一些常用属性和方法
| 方法 | 说明 |
|---|---|
| setItem(key, value) | 以提供的 key-value,写入 storage 中 |
| getItem(key) | 以提供的 key,读取 storage 中的 value 并返回 |
| length | storage 的长度 |
| key(index) | 以提供的 index,获取指定的 key |
| removeItem(key) | 以提供的 key,删除某一项 |
| clear() | 清空 storage |
2.2 IndexedDB
DB 一词,意为数据库(database),通常情况下在服务器端很常见。在实际开发中其实很少用到 IndexedDB,如果需要用到本地储存,使用 storage 就能满足我们,但是如果有大量的数据需要储存的话,就可以用到 IndexedDB。这个是 MDN 文档
IndexedDB 是一种底层 API,用于在客户端储存大量结构化数据库:
- 它是一种事务型数据库系统,是一种基于 JS 面向对象数据库,有点类似于 NoSQL(非关系型数据库)
- IndexedDB 本身是基于事务的,我们只需要执行数据库模式,打开与数据库的连接,然后检索和更新一系列事物即可
- IndexedDB 的所有操作都是异步的,避免阻塞应用程序的执行。
- 在浏览器隐私模式下完全禁用 IndexedDB
连接 IndexedDB
const request = window.indexedDB.open(name, version?)
使用 open 方法来连接一个数据库,需要传入数据库的名称 name,如果该数据库未创建则会自动创建,该方法会返回一个 IDBRequest 对象,通过该对象来异步操作 IndexedDB,version 必须是整数,比如 3.4 会被视为3,非必填
在产生每一个请求后,都要为这个请求添加成功与失败函数
let db
const request = window.indexedDB.open(name, version?)
request.onerror = function(event) {
// 在失败函数时进行自定义处理
};
request.onsuccess = function(event) {
// 在成功函数保存 db 对象
db = evenet.target.result
};
创建和更新数据库版本号
当你创建了一个新的数据库或者增加已有数据库的版本号时(当打开数据库,指定一个比之前更大的版本),onupgradeneeded 事件会被触发,需要在这个阶段来创建该版本需要的对象仓库
request.onupgradeneeded = event => {
// 保存 IDBDataBase 接口
const db = event.target.result
// 为该数据库创建一个对象仓库
const objectStore = db.createObjectStore('user', { keyPath: 'id' })
}
浏览器会保存之前版本的对象仓库,只需要创建新版本的数据库或删除之前版本不需要的数据库即可。这里的 keyPath 就类似于数据库中的主键,是唯一的。如果在新版本中想要修改主键,必须要删除之前的数据库,并创建新的数据库(注意:这样会丢失数据,请确保做好备份)。如果 onupgradeneeded 执行完成,就会调用 onsuccess
构建数据库
IndexedDB 使用对象仓库而不是数据表,并且一个单独的数据库可以包含任意数量的对象存储空间。每当一个值被存储进一个对象存储空间时,它会被和一个键相关联。键的提供可以有几种不同的方法,这取决于对象存储空间是使用 key path 还是 key generator
下面的表格显示了几种不同的提供键的方法。
| 键路径 key path | 键生成器 key generator | 描述 |
|---|---|---|
| No | No | 这种对象存储空间可以存储任意类型的数据。每当我们新增一个值时,需要提供单独的键参数 |
| Yes | No | 这种对象存储空间只能存储 JS 对象类型的数据,这些对象必须有一个 key path 同名属性 |
| No | Yes | 这种对象存储空间可以存储任意类型数据,键会自动生成,或者可以提供一个特定的键参数 |
| Yes | Yes | 这种对象存储空间只能存储 JS 对象类型的数据,通常一个键被生成的同时,生成的键会放在对象中和 key path 同名的属性中。如果这个值存在,那么就会用这个值,不会再生成 |
这里的键路径和键生成器就像是索引,通过索引可以查询对应的值,而不是对对象的键来查找。此外,由于索引是 unique 的,所以可以限制存储重复索引的数据
// 假设我们有下面的客户数据
const customerData = [
{ ssn: '444-44-4444', name: 'Bill', age: 35, email: 'bill@company.com' },
{ ssn: '555-55-5555', name: 'Donna', age: 32, email: 'donna@home.org' },
]
我们肯定不愿意使用社会保险号(ssn)来作为客户表的主键,ssn 不是一个必定有值的字段,而且应该储存用户的生日而不是年龄。让我们忽略掉这不合理的设计,看看如何储存这些数据。
// 假设我们有下面的客户数据
const customerData = [
{ ssn: '444-44-4444', name: 'Bill', age: 35, email: 'bill@company.com' },
{ ssn: '555-55-5555', name: 'Donna', age: 32, email: 'donna@home.org' },
]
const dbName = 'the_name'
// 创建连接
const request = indexedDB.open(dbName, 2)
request.onerror = function (event) {
// 错误处理
}
// 第一次打开时创建
request.onupgradeneeded = function (event) {
const db = event.target.result
// 建立一个对象仓库来存储我们客户的相关信息,我们选择 ssn 作为键路径(key path)
// 因为 ssn 可以保证是不重复的
const objectStore = db.createObjectStore('customers', { keyPath: 'ssn' })
// 建立一个索引来通过姓名来搜索客户。名字可能会重复,所以我们不能使用 unique 索引
objectStore.createIndex('name', 'name', { unique: false })
// 使用邮箱建立索引,确保客户的邮箱不会重复,所以我们使用 unique 索引
objectStore.createIndex('email', 'email', { unique: true })
// 使用事务的 oncomplete 事件确保在插入数据前对象仓库已经创建完毕
objectStore.transaction.oncomplete = function (event) {
// 将数据保存到新创建的对象仓库
const customerObjectStore = db
.transaction('customers', 'readwrite')
.objectStore('customers')
customerData.forEach(function (customer) {
customerObjectStore.add(customer)
})
}
}
正如前面提到的,onupgradeneeded 是我们唯一可以修改数据库结构的地方。在这里面,我们可以创建和删除对象存储空间以及构建和删除索引。
对象仓库仅调用 createObjectStore() 就可以创建。这个方法使用仓库的名称,和一个参数对象。即便这个参数对象是可选的,它还是非常重要的,因为它可以让你定义重要的可选属性,并完善你希望创建的对象存储空间的类型。在我们的示例中,我们创建了一个名为 “customers” 的对象仓库并且定义了一个使得每个仓库中每个对象都独一无二的 keyPath 。在这个示例中的属性是 “ssn”,因为社会安全号码被确保是唯一的。被存储在该仓库中的所有对象都必须存在 “ssn”。
我们也请求了一个名为 “name” 的着眼于存储的对象的 name 属性的索引。如同 createObjectStore(),createIndex() 提供了一个可选地 options 对象,该对象细化了我们希望创建的索引类型。新增一个不带 name 属性的对象也会成功,但是这个对象不会出现在 "name" 索引中。
我们现在可以使用存储的用户对象的 ssn 直接从对象存储空间中把它们提取出来,或者通过使用索引来使用他们的 name 进行提取。要了解这些是如何实现的,请参见 使用索引 章节。
使用键生成器
在创建对象仓库时设置 autoIncrement 标记会为该仓库开启键生成器。默认该设置是不开启的。
使用键生成器,当你向对象仓库新增记录时键会自动生成。对象仓库生成的键往往从 1 开始,然后自动生成的新的键会在之前的键的基础上加 1。生成的键的值从来不会减小,除非数据库操作结果被回滚,比如,数据库事务被中断。因此删除一条记录,甚至清空对象仓库里的所有记录都不会影响对象仓库的键生成器。
我们可以使用键生成器创建一个对象仓库:
// 打开 indexedDB.
const request = indexedDB.open(dbName, 3);
request.onupgradeneeded = function (event) {
var db = event.target.result;
// 设置 autoIncrement 标志为 true 来创建一个名为 names 的对象仓库
var objStore = db.createObjectStore("names", { autoIncrement : true });
// 因为 names 对象仓库拥有键生成器,所以它的键会自动生成。
// 被插入的数据可以表示如下:
// key : 1 => value : "Bill"
// key : 2 => value : "Donna"
customerData.forEach(function(customer) {
objStore.add(customer.name);
});
};
更多关于键生成器的细节,请查阅 "W3C Key Generators"。
增加数据
你需要开启一个事务才能对你的创建的数据库进行操作。事务来自于数据库对象,而且你必须指定你想让这个事务跨越哪些对象仓库。一旦你处于一个事务中,你就可以目标对象仓库发出请求。你要决定是对数据库进行更改还是只需从中读取数据。事务提供了三种模式:readonly、readwrite 和 versionchange。
想要修改数据库模式或结构 —— 包括新建或删除对象仓库或索引,只能在 versionchange 事务中才能实现。该事务由一个指定了 version 的 IDBFactory.open 方法启动。(在仍未实现最新标准的 WebKit 浏览器 ,IDBFactory.open 方法只接受一个参数,即数据库的 name,这样你必须调用 IDBVersionChangeRequest.setVersion 来建立 versionchange 事务。
使用 readonly 或 readwrite 模式都可以从已存在的对象仓库里读取记录。但只有在 readwrite 事务中才能修改对象仓库。你需要使用 IDBDatabase.transaction启动一个事务。
该方法接受两个参数:
storeNames(作用域,一个你想访问的对象仓库的数组),- 事务模式
mode(readonly 或 readwrite)。该方法返回一个包含IDBIndex.objectStore方法的事务对象,使用IDBIndex.objectStore你可以访问你的对象仓库。未指定mode时,默认为readonly模式。
var transaction = db.transaction(["customers"], "readwrite");
// 在所有数据添加完毕后的处理
transaction.oncomplete = function(event) {
alert("All done!");
};
transaction.onerror = function(event) {
// 不要忘记错误处理!
};
// 获取对象仓库[customers]
var objectStore = transaction.objectStore("customers");
customerData.forEach(function(customer) {
// 遍历 [customerData],并逐条添加到对象仓库中
var request = objectStore.add(customer);
// 添加成功后,即可触发 onsuccess
request.onsuccess = function(event) {
// event.target.result 就是对象仓库的键【主键】
// event.target.result === customer.ssn;
};
});
删除数据
删除数据和添加数据也是类似的
var request = db.transaction(["customers"], "readwrite")
.objectStore("customers")
.delete("444-44-4444");
request.onsuccess = function(event) {
// 删除成功!
};
// delete 方法接收的是主键
获取数据
var transaction = db.transaction(["customers"]);
var objectStore = transaction.objectStore("customers");
// 使用 get 并提供主键,获取该条数据
var request = objectStore.get("444-44-4444");
request.onerror = function(event) {
// 错误处理!
};
request.onsuccess = function(event) {
// 对 request.result 做些操作!
alert("Name for SSN 444-44-4444 is " + request.result.name);
};
对于一个简单的提取来说,代码看起来会有点多,所以也可以来简化一下:
db.transaction("customers").objectStore("customers").get("444-44-4444").onsuccess = function(event) {
alert("Name for SSN 444-44-4444 is " + event.target.result.name);
};
更新数据
var objectStore = db.transaction(["customers"], "readwrite").objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function(event) {
// 错误处理
};
request.onsuccess = function(event) {
// 获取我们想要更新的数据
var data = event.target.result;
// 更新你想修改的数据
data.age = 42;
// 把更新过的对象放回数据库
var requestUpdate = objectStore.put(data);
requestUpdate.onerror = function(event) {
// 错误处理
};
requestUpdate.onsuccess = function(event) {
// 完成,数据已更新!
};
};
使用游标
使用 get() 要求你知道你想要检索哪一个键。如果你想要遍历对象存储空间中的所有值,那么你可以使用游标。看起来会像下面这样:
var objectStore = db.transaction("customers").objectStore("customers");
objectStore.openCursor().onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor.continue();
}
else {
alert("No more entries!");
}
};
openCursor() 该函数需要接收一些参数:
- 首先,需要一个
key range来限制被检索的范围 - 第二,需要指定你希望的迭代方向,在上面的示例中,我们在以升序迭代所有的对象。
游标的回调会比较特殊,游标对象本身是请求的 result (上面我们使用的是简写形式,所以是 event.target.result)。。然后实际的 key 和 value 可以根据游标对象的 key 和 value 属性被找到。如果你想要保持继续前行,那么你必须调用游标上的 continue() 。当你已经到达数据的末尾时(或者没有匹配 openCursor() 请求的条目)你仍然会得到一个成功回调,但是 result 属性是 undefined。
使用游标的一种常见模式是提取出在一个对象存储空间中的所有对象然后把它们添加到一个数组中,像这样:
var customers = [];
objectStore.openCursor().onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
customers.push(cursor.value);
cursor.continue();
}
else {
alert("以获取所有客户信息: " + customers);
}
};
也可以使用 getAll() 的方式来处理上述的情况,实际的效果和上面是一样的,不过要注意一点:使用游标要比 getAll() 效率要高得多。
objectStore.getAll().onsuccess = function(event) {
alert("Got all customers: " + event.target.result);
};
使用索引
使用 SSN 作为键来存储客户数据是合理的,因为 SSN 唯一地标识了一个个体。如果你想要通过姓名来查找一个客户,那么,你将需要在数据库中迭代所有的 SSN 直到你找到正确的那个。以这种方式来查找将会非常的慢,相反你可以使用索引。
// 首先,确定你已经在 request.onupgradeneeded 中创建了索引:
// objectStore.createIndex("name", "name");
// 否则你将得到 DOMException。
var index = objectStore.index("name");
index.get("Donna").onsuccess = function(event) {
alert("Donna's SSN is " + event.target.result.ssn);
};
“name” 游标不是唯一的,因此 name 被设成 "Donna" 的记录可能不止一条。在这种情况下,你总是得到键值最小的那个。
如果你需要访问带有给定 name 的所有的记录你可以使用一个游标。你可以在索引上打开两个不同类型的游标。一个常规游标映射索引属性到对象存储空间中的对象。一个键索引映射索引属性到用来存储对象存储空间中的对象的键。不同之处被展示如下:
index.openCursor().onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
// cursor.key 是一个 name, 就像 "Bill", 然后 cursor.value 是整个对象。
alert("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ", email: " + cursor.value.email);
cursor.continue();
}
};
index.openKeyCursor().onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
// cursor.key 是一个 name, 就像 "Bill", 然后 cursor.value 是那个 SSN。
// 没有办法可以得到存储对象的其余部分。
alert("Name: " + cursor.key + ", SSN: " + cursor.value);
cursor.continue();
}
};
指定游标的范围和方向
如果你想要限定你在游标中看到的值的范围,你可以使用一个 key range 对象然后把它作为第一个参数传给 openCursor() 或是 openKeyCursor()。你可以构造一个只允许一个单一 key 的 key range,或者一个具有下限或上限,或者一个既有上限也有下限。边界可以是 “闭合的”(也就是说 key range 包含给定的值)或者是 “开放的”(也就是说 key range 不包括给定的值)。这里是它如何工作的:
// 仅匹配 "Donna"
var singleKeyRange = IDBKeyRange.only("Donna");
// 匹配所有超过“Bill”的,包括“Bill”
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");
// 匹配所有超过“Bill”的,但不包括“Bill”
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);
// 匹配所有不超过“Donna”的,但不包括“Donna”
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);
// 匹配所有在“Bill”和“Donna”之间的,但不包括“Donna”
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);
// 使用其中的一个键范围,把它作为 openCursor()/openKeyCursor 的第一个参数
index.openCursor(boundKeyRange).onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
// 当匹配时进行一些操作
cursor.continue();
}
};
有时候你可能想要以倒序而不是正序(所有游标的默认顺序)来遍历。切换方向是通过传递 prev 到 openCursor() 方法来实现的:
objectStore.openCursor(boundKeyRange, "prev").onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
// 进行一些操作
cursor.continue();
}
};
如果你只是想改变遍历的方向,而不想对结果进行筛选,你只需要给第一个参数传入 null。
objectStore.openCursor(null, "prev").onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
// Do something with the entries.
cursor.continue();
}
};
因为 “name” 索引不是唯一的,那就有可能存在具有相同 name 的多条记录。要注意的是这种情况不可能发生在对象存储空间上,因为键必须永远是唯一的。如果你想要在游标在索引迭代过程中过滤出重复的,你可以传递 nextunique (或 prevunique 如果你正在向后寻找)作为方向参数。 当 nextunique 或是 prevunique 被使用时,被返回的那个总是键最小的记录。
index.openKeyCursor(null, IDBCursor.nextunique).onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
cursor.continue();
}
};
一个完整的例子
总结
在本文中,详细介绍了 JSON、Web Storage 以及 IndexedDB 的相关知识,希望会对你有所帮助