如何用IndexedDB在浏览器中存储无限的数据

714 阅读7分钟

How to Store Unlimited* Data in the Browser with IndexedDB

本文解释了使用IndexedDB API在浏览器中存储数据的基本原理,它的容量远远大于其他客户端机制。

存储Web应用的数据曾经是一个简单的决定。除了将数据发送到服务器上,更新数据库外,没有其他选择。今天,有一系列的选择,数据可以被存储在客户端。

为什么要在浏览器中存储数据?

将大多数用户生成的数据存储在服务器上是很实用的,但也有例外。

  • 设备特定的设置,如用户界面选项、光/暗模式等。
  • 短暂的数据,比如在选择上传照片之前拍摄一系列的照片
  • 脱机数据,以便以后同步,也许是在连接有限的地区
  • 渐进式网络应用程序(PWA),出于实际或隐私的原因在离线状态下运行
  • 缓存资产以提高性能

三个主要的浏览器API可能适合。

  1. 网络存储

    在当前会话期间或之后,简单的同步名-值对存储。这对较小的、不那么重要的数据来说很实用,比如用户界面偏好。浏览器允许每个域有5MB的网络存储。

  2. 缓存API

    HTTP请求和响应对象对的存储。该API通常被服务工作者用来缓存网络响应,因此渐进式网络应用程序可以更快地执行和离线工作。浏览器有所不同,但iOS上的Safari分配了50MB。

  3. 索引数据库

    一个客户端的NoSQL数据库,可以存储数据、文件和Blobs。浏览器各不相同,但每个域至少要有1GB,它可以达到剩余磁盘空间的60%。

好吧,我说谎了。IndexedDB并不提供无限的存储空间,但它的限制远远小于其他选项。它是较大的客户端数据集的唯一选择。

IndexedDB简介

IndexedDB于2011年首次出现在浏览器中。该API在2015年1月成为W3C标准,并在2018年1月被API 2.0所取代。API 3.0正在进行中。因此,IndexedDB有很好的浏览器支持,并在标准脚本和Web Workers中可用。受虐狂的开发者甚至可以在IE10中尝试。

Data on support for the indexeddb feature across the major browsers from caniuse.com

本文引用了以下数据库和 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的浏览器都提供了一个应用选项卡,你可以在那里检查存储空间,人为地限制容量,并擦除所有数据。

DevTools Application panel

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

DevTools IndexedDB storage

(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() 方法为对象存储定义了两个新的索引。

  1. dateIdx 在每条记录中的date
  2. tagsIdx 在每条记录中的 阵列上(一个 索引,将单个阵列项扩展为一个索引)。tags multiEntry

我们有可能有两个具有相同日期或标签的笔记,所以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 );

};