本文解释了使用IndexedDB API在浏览器中存储数据的基本原理,它的容量远远大于其他客户端机制。
存储Web应用的数据曾经是一个简单的决定。除了将数据发送到服务器上,更新数据库外,没有其他选择。今天,有一系列的选择,数据可以被存储在客户端。
为什么要在浏览器中存储数据?
将大多数用户生成的数据存储在服务器上是很实用的,但也有例外。
- 设备特定的设置,如用户界面选项、光/暗模式等。
- 短暂的数据,比如在选择上传照片之前拍摄一系列的照片
- 脱机数据,以便以后同步,也许是在连接有限的地区
- 渐进式网络应用程序(PWA),出于实际或隐私的原因在离线状态下运行
- 缓存资产以提高性能
三个主要的浏览器API可能适合。
-
在当前会话期间或之后,简单的同步名-值对存储。这对较小的、不那么重要的数据来说很实用,比如用户界面偏好。浏览器允许每个域有5MB的网络存储。
-
HTTP请求和响应对象对的存储。该API通常被服务工作者用来缓存网络响应,因此渐进式网络应用程序可以更快地执行和离线工作。浏览器有所不同,但iOS上的Safari分配了50MB。
-
一个客户端的NoSQL数据库,可以存储数据、文件和Blobs。浏览器各不相同,但每个域至少要有1GB,它可以达到剩余磁盘空间的60%。
好吧,我说谎了。IndexedDB并不提供无限的存储空间,但它的限制远远小于其他选项。它是较大的客户端数据集的唯一选择。
IndexedDB简介
IndexedDB于2011年首次出现在浏览器中。该API在2015年1月成为W3C标准,并在2018年1月被API 2.0所取代。API 3.0正在进行中。因此,IndexedDB有很好的浏览器支持,并在标准脚本和Web Workers中可用。受虐狂的开发者甚至可以在IE10中尝试。

本文引用了以下数据库和 IndexedDB 术语。
-
数据库:顶层存储。可以创建任何数量的 IndexedDB 数据库,尽管大多数应用程序会定义一个。数据库的访问被限制在同一域内的页面;甚至子域也被排除在外。例如:你可以为你的笔记应用程序创建一个
notebook数据库。 -
对象存储:相关数据项的名称/价值存储,概念上类似于MongoDB中的集合或SQL数据库中的表。你的
notebook数据库可以有一个note对象存储来保存记录,每个记录都有一个ID、标题、正文、日期和一个标签阵列。 -
key:一个唯一的名字,用于引用对象存储中的每一条记录(值)。它可以自动生成或设置为记录中的一个值。ID是作为
note商店的键的理想选择。 -
autoIncrement:定义的键可以在每次有记录被添加到商店时自动增加其值。
-
索引:告诉数据库如何在一个对象存储中组织数据。必须创建一个索引来使用该数据项作为标准进行搜索。例如,笔记
date,可以按时间顺序建立索引,这样就有可能找到特定时期的笔记。 -
模式:数据库内对象存储、键和索引的定义。
-
版本:分配给模式的版本号(整数),以便必要时可以更新数据库。
-
操作:一个数据库活动,如创建、读取、更新或删除(CRUD)一条记录。
-
事务:一个或多个操作的包装,保证数据的完整性。数据库要么运行事务中的所有操作,要么不运行:它不会运行一些操作而失败其他操作。
-
游标:一种在许多记录上进行迭代的方法,而不需要一次性将所有记录加载到内存中。
-
异步执行。IndexedDB的操作是异步运行的。当一个操作开始时,比如获取所有笔记,该活动在后台运行,其他的JavaScript代码继续运行。当结果准备好时,会调用一个函数。
下面的例子将笔记记录--如以下内容--存储在一个名为notebook 的数据库中的note 对象存储中。
{
id: 1,
title: "My first note",
body: "A note about something",
date: <Date() object>,
tags: ["#first", "#note"]
}
IndexedDB的API有点过时,它依赖于事件和回调。它并不直接支持 ES6 语法上的可爱之处,例如 Promises 和async/await 。诸如idb这样的封装库是可用的,但本教程会深入到金属。
IndexDB DevTools 调试
我相信你的代码是完美的,但我犯了很多错误。即使是这篇文章中的简短片段也被重构了很多次,而且我一路走来毁掉了几个 IndexedDB 数据库。浏览器开发工具是无价之宝。
所有基于Chrome的浏览器都提供了一个应用选项卡,你可以在那里检查存储空间,人为地限制容量,并擦除所有数据。

存储树中的IndexedDB条目允许你检查、更新和删除对象存储、索引和单个记录。

(Firefox也有一个类似的面板,名为Storage。)
另外,你可以在隐身模式下运行你的应用程序,这样当你关闭浏览器窗口时,所有数据都会被删除。
检查对索引数据库的支持
window.indexedDB 当一个浏览器支持IndexedDB时,评估 。true
if ('indexedDB' in window) {
// indexedDB supported
}
else {
console.log('IndexedDB is not supported.');
}
很少遇到不支持IndexedDB的浏览器。一个应用程序可以退回到较慢的、基于服务器的存储,但大多数会建议用户升级他们的十年前的应用程序!
检查剩余的存储空间
基于承诺的StorageManager API提供了对当前域的剩余空间的估计。
(async () => {
if (!navigator.storage) return;
const
required = 10, // 10 MB required
estimate = await navigator.storage.estimate(),
// calculate remaining storage in MB
available = Math.floor((estimate.quota - estimate.usage) / 1024 / 1024);
if (available >= required) {
console.log('Storage is available');
// ...call functions to initialize IndexedDB
}
})();
这个API在IE或Safari中不被支持(还没有),所以当navigator.storage 不能返回一个虚假的值时,要警惕。
通常情况下,接近1000兆的自由空间是可用的,除非设备的驱动器正在耗尽。Safari可能会提示用户同意更多,尽管PWA无论如何都会被分配1GB。
随着使用限制的达到,一个应用程序可以选择。
- 删除旧的临时数据
- 要求用户删除不必要的记录,或
- 将较少使用的信息转移到服务器上*(对于真正的无限存储!)。*
打开一个 IndexedDB 连接
一个 IndexedDB 连接被初始化为 indexedDB.open().它被传递。
- 数据库的名称,以及
- 一个可选的版本整数
const dbOpen = indexedDB.open('notebook', 1);
这段代码可以在任何初始化块或函数中运行,通常是在你检查了对 IndexedDB 的支持之后。
当第一次遇到这个数据库时,所有的对象存储和索引必须被创建。一个onupgradeneeded 事件处理函数获得数据库连接对象 (dbOpen.result) 并运行方法,如 createObjectStore()等方法,以备不时之需。
dbOpen.onupgradeneeded = event => {
console.log(`upgrading database from ${ event.oldVersion } to ${ event.newVersion }...`);
const db = dbOpen.result;
switch( event.oldVersion ) {
case 0: {
const note = db.createObjectStore(
'note',
{ keyPath: 'id', autoIncrement: true }
);
note.createIndex('dateIdx', 'date', { unique: false });
note.createIndex('tagsIdx', 'tags', { unique: false, multiEntry: true });
}
}
};
这个例子创建了一个名为note 的新对象存储空间。第二个参数(可选)指出,每条记录中的id 值可以作为存储空间的键,并且每当有新的记录加入时,它可以自动递增。
createIndex() 方法为对象存储定义了两个新的索引。
dateIdx在每条记录中的datetagsIdx在每条记录中的 阵列上(一个 索引,将单个阵列项扩展为一个索引)。tagsmultiEntry
我们有可能有两个具有相同日期或标签的笔记,所以unique 被设置为false。
注意:这个开关语句看起来有点奇怪,而且没有必要,但在升级模式时,它将变得有用。
一个onerror 处理程序报告任何数据库连接错误。
dbOpen.onerror = err => {
console.error(`indexedDB error: ${ err.errorCode }`);
};
最后,当连接建立后,一个onsuccess 处理程序运行。连接 (dbOpen.result) 用于所有进一步的数据库操作,所以它可以被定义为一个全局变量或传递给其他函数(如main() ,如下所示)。
dbOpen.onsuccess = () => {
const db = dbOpen.result;
// use IndexedDB connection throughout application
// perhaps by passing it to another function, e.g.
// main( db );
};
