Indexed Database使用指南

1,081 阅读35分钟

前言

本篇文章是关于indexDB我的一些学习分享,学习内容来自indexDB W3C规范,大家可以直接查看规范学习,如果对英文规范有理解上的困难,亦可查阅此篇文章学习,这里对规范的内容做了提炼总结,并附上一些实践案例,如果通过阅读此文让你对indexDB的开发实践有比较透彻的理解,那真是一件令人快乐的事儿~

背景

在实际业务开发中,我们通常会有一些大量复杂的数据结构的请求和处理,这些数据如果通过接口获取会比较耗费带宽,影响性能,且一次性加载大量数据在内存中,会对浏览器造成较大的内存压力。这时候就需要一个比localStorage存储量更大的,存储结构更灵活的存储机制,Indexed Database就是其中一种。

它通常是通过使用持久性的B树的数据结构来实现的,这些数据结构被认为对于数据的插入、删除以及大量数据的有序遍历更高效。

1. 简介

入门🌰(在线地址):

const request = indexedDB.open('indexdb_init', 1);
let db;
request.onupgradeneeded = function(event) {
  // 多版本并行
  const db = request.result;
  if(event.oldVersion < 1) {
    const store = db.createObjectStore("books", { keyPath: 'isbn' });
    db.books = store;
    const titleIndex = store.createIndex("by_title", "title", { unique: true });
    const authorIndex = store.createIndex("by_author", "author");

  }
  if(event.oldVersion < 2) {
    const bookStore = request.transaction?.objectStore("books");
    const yearIndex = bookStore?.createIndex("by_year", "year");
  }
  if(event.oldVersion < 3) {
    const magazines = db.createObjectStore("magazines");
    const publisherIndex = magazines.createIndex("by_publisher", "publisher");

  }

}
request.onsuccess = function() {
  db = request.result;
  console.log(db, db.parent, 'd-------')
}

request.onerror = function(e) {
  console.log("open database error", e);
}

多个客户端(pages 和 workers)可以同时使用一个数据库,transaction(事务)保证他们不会在读取和写入的时候不会发生冲突,如果一个客户端想要升级数据库(通过 upgradeneeded 事件),只有当所有其他客户端关闭他们与数据库当前版本的连接才可以。

为了避免阻止数据库升级,客户端可以监听versionchange事件,当其中一个页面或者线程想要升级数据库时触发,为了让这种情况继续下去,请通过执行最终关闭此页面(线程)与数据库连接的操作来响应versionchange

其中一种方式是重载页面。

db.onversionchange = function() {
  // 第一步, 保存数据:
  saveUnsavedData().then(function() {
    // 如果页面处于未激活状态,那么在没有用户交互的情况下重新加载页面是合适的
    if (!document.hasFocus()) {
      location.reload();
      // 重新加载会关闭数据库,并重新加载新的js和数据库定义
    } else {
      // 如果页面处于激活状态,重新加载页面可能会造成干扰,可能需要要求用户手动完成
      displayMessage("Please reload this page for the latest version.");
    }
  });};
  function saveUnsavedData() {
 
  }
  function displayMessage() {
  }

另一种方法是调用连接的close方法,但是,你需要确保你的应用知道这一点,因为后续访问数据库的尝试都将失败。

db.onversionchange = function() {
  saveUnsavedData().then(function() {
    db.close();
    stopUsingTheDatabase();
  });
};
function stopUsingTheDatabase() {
}

尝试升级的页面可以使用阻塞事件来检测其他客户端是否正在阻塞升级发生,如果其他客户端在versionchange事件后仍保持与数据库的连接,则会触发阻塞事件。

// 创建基于新版本的数据库连接,触发versionchange事件
const request = indexedDB.open("library", 4);

request.onblocked = function() {
  // 检测到有数据库升级事件,执行保存数据的逻辑
  blockedTimeout = setTimeout(function() {
    displayMessage(
        "Upgrade blocked - Please close other tabs displaying this site."
    );
  }, 1000);};

request.onupgradeneeded = function(event) {
  clearTimeout(blockedTimeout);
  // 一些善后工作
  hideMessage();
  // ...
};
function hideMessage() {
  // Hide a previously displayed message.
}

2. 结构

数据库的关键属性name(数据库名称),数据库名称对大小写敏感。

2.1 Database

每个域名下可以创建多个数据库,每个数据库具有零个或多个Object Store(可以理解为数据表),这些Object Store被用来存储数据。

Database有name属性,是一个常量,在数据库的生命周期内保持不变。

Database有version属性,数据库第一次被创建时,version默认为0。一个数据库同一时刻只会存在一个版本,使用upgrade transaction(触发数据库升级的事务)是更改数据库版本的唯一方式。

js通过建立连接来操作数据库,同一时刻,对于给定的数据库可能存在多个连接。只能建立与当前域名作用域下的数据库的连接。

每个连接有一个初始化为false的close pending flag。当连接关闭后会被设置为true

在特殊情况下,浏览器端会关闭连接,例如由于无法访问文件系统,权限更改,或数据库被删除等。如果发生这种情况,浏览器端必须运行关闭数据库连接,并将连接close pending flag设置为true

一个数据库连接可以操作该数据库下的Object Store,如果尝试升级或删除数据库,会触发打开连接的versionchange事件,这使得连接有机会关闭以允许升级或删除数据库继续进行。

2.2 Object Store

Object Store是数据库存储的主要机制,一个Object Store包含存储在该store中的一系列记录,每个记录包含一个key和一个value,这些数据以key升序的规则存储。

一个Object Store有以下特性:

  • name

Object Storename属性,Object store的命名是不能重复的。

  • 主键

一个Object Store对象有一个可选的keypath,如果一个store设置了主键,意思它要使用 in-line-keys 否则它将使用out-of-line keys

  • 当一个Store(数据集合)被创建的时候,它可以使用一个自动生成的主键来区分每一条记录。如果一个数据库没有设置主键,将会为他引入key生成器来区分插入到这个store的记录。一个store的主键有三个来源:

    • 主键生成器: 每次新的记录插入的时候为其生成一个自增的数字。
    • 主键可以通过keyPath来主动设置。
    • 主键可以根据存储的数据来明确的区分。例如: 日期对象,文件对象,二进制对象,图片对象等等。
  • Object Store handle

  1. js不直接与Object Store交互,而是通过事务中通过Object Store handle间接访问。可以针对同一个Object Store创建多个事务,一个事务仅能有一个关联的Object Store。

一个🌰(在线地址):

const request = indexedDB.open("objectStoreHandle");
let db;
request.onsuccess = function(event) {
  db = request.result;
  console.log(event, 'success-----')
}
request.onerror = function(event) {
  console.log(event, 'error------')
}
request.onblocked = function(event) {
  console.log(event, 'error------')
}
request.onupgradeneeded = function(event) {
   // 在这个upgrade transaction里可以创建数据表和管理数据表的索引
  const db1 = event.target.result;
  // object store handle
  const objectStore = db1.createObjectStore("idbobjectStore", {  autoIncrement: true });
    // 使用 object store handle 创建索引
    objectStore.createIndex("name", "name", { unique: false });
    objectStore.createIndex("age", "age");
}

setTimeout(() => {
  const transaction = db.transaction("idbobjectStore", "readwrite");
  transaction.oncomplete = function(event) {
    console.log('transaction committed-----', event)
  }
  transaction.onerror = function (event) {
    console.log('transaction error-----', event)
  }
  transaction.onabort = function (event) {
    console.log('transaction abort-----', event)
  }
  console.log(transaction, transaction.parent, 'transaction-----')
  // object store handle
  const objStore = transaction.objectStore("idbobjectStore");
  // 可以调用 objStore的属性和方法操作数据表里存储的数据
  console.log(objStore, 'objStore-----')
}, 2000)
console.log(request, 'request----')


2.3 Values

支持存储的数据类型

  • String

  • Number

  • Object

  • Array

  • Date Object

  • File Object

  • Blob Object

  • ImageData Object

value的存取都是基于值而不是引用,所以后面基于该对象的修改都不会影响数据库里存的对应记录。

2.4 Keys

为了更高效的存取数据,object store中的记录都是按照主键 or 索引 升序存储。

一个key也有一个关联的值,如果类型是number或者date,则为不受限制的双精度浮点数(不受限制的双精度类型是浮点数类型,对应于所有可能的双精度64位的IEEE754浮点数,finitenon-finite和特殊的非数字值NaN的集合)。

ECMAScript 类型都是合法的keys

  • 数字类型: 除了NaN都支持,包括 Infinity-Infinity

  • 日期类型:除非这个DataValueinter slotNaN

  • 字符串类型

  • ArrayBuffer 对象

  • 数组对象

keys的比较逻辑:

-infinity 是一个key的最小值,比较逻辑是:

数字类型的键值 < 日期类型的键值,日期类型的键值 < 字符串类型的键值,字符串类型的键值 < 二进制类型的键值,二进制类型的键值 < 数组类型的键值,所以可以设定数据库的最大值是[],最小值是-infinity

二进制keys 的比较范围是无符号数 0-255(包含首尾)而不是有符号数-128-127(包含首尾)

2.5 Key Path

主键是一个字符串或者一个字符串列表,能够唯一确定一条数据记录。合法的主键:

  • 一个空字符串

  • 一个标识符,符合ECMAScript语法规范的标识符

  • 逗号分隔的标识符

  • 一个非空的字符串数组,字符串符合以上约束。

空格不允许作为一个组件

2.6 Index

我们可以借助索引来实现对数据库更快的查询、更新和删除。

索引具有以下属性:

  • name

一个Store创建的索引名称应该是确定且唯一的,

  • unique

如果设置该属性为true,则插入或者更新的索引值和数据库中已有的记录重复会失败。

  • multiEntry

如果设置为false,则会为其创建一个数组索引。如果设置为true,则一个记录会为这个数组索引的每个子元素创建索引。

js不直接和索引交互,而是通过创建事务来操作索引。一个事务只能绑定一个index处理函数。

一个index处理函数有一个名字,这个名字知道一个upgrade 事务触发前都是有效的。

2.7 事务

我们通过创建事务来读/写数据库中的数据。

事务可以保证我们多个数据库读写操作的有序进行,一个事务可能被用来存储大量数据或者有条件去修改一些数据,事务表示一组原子的、持久的数据访问和数据变更操作。

一个事务有一个作用域,它是一个事务可以与之交互的对象集合。这个作用域会一直保持有效直到数据库版本升级的事务触发。一个事务有以下模式:

  • readonly

这种类型的事务只允许读取数据,数据库打开,即可创建这种类型的事务,这种类型的事务有一个优势就是可以同一时间创建作用域有重叠甚至在同一个作用域的多个事务。

  • readwrite

这种类型的事务允许读取、修改和删除数据。数据库打开,即可创建这种类型的事务,这种类型的事务不支持同一时间创建多个作用域有重叠的多个事务。

  • versionchange

这种类型的事务允许读取、修改和删除数据。也能够创建和删除Stores(数据表)和索引。这种类型的事务会在一个upgradeneeded事件被触发的时候自动创建,不能够手动创建。

一个事务有一个持久性标记,这个持久性标记有以下取值:

  • strict

只有在验证所有未完成的更改都已成功写入持久存储介质后,用户代理才可以认为事务已成功提交。

  • relaxed

一旦所有未完成的修改都成功写入存储介质后,用户代理可以任务事务已成功提交,无需后续验证。

  • default

用户代理对storage bucket使用其默认的持久性行为,这是事务在没有另外指定下的默认值。

鼓励 Web 应用程序对临时数据(例如缓存或快速更改的记录)使用relaxed模式,而在降低数据丢失风险大于对性能和电源的影响的情况下使用strict模式。鼓励实现权衡来自应用程序的持久性提示与对用户和设备的影响。

事务还有 waitUntil 方法?

2.7.1 事务的生命周期

一个事务有以下状态:

  • active

当事务首次创建,或者基于此事务派发一个事件时该事务会进入这个状态,当事务处于这个状态时,可以针对该事务发起新请求。

  • inactive

事务在其创建后控制权返回到事件循环之后,以及不再有新的请求发起时,就处于这种状态。

当事务处于此状态时,不能对事务提出任何请求。

  • committing

一旦与事务关联的所有请求都完成后,事务将在尝试提交时进入此状态。当事务处于此状态时,不能对事物提出任何请求。

  • finished

一旦一个事物已经提交或者被迫中止,就会进入这个状态。当事务处于此状态时,不能对事物提出任何请求。

我们能够创建一个较长时间的事务,但这不是推荐的做法,因为它可能会带来不好的用户体验。一个事务的生命周期如下:

  1. 一个事务被创建基于对应的模式和作用域,一个事务在创建的时候就进入激活状态。

  2. 当一个实现能够对下面定义的事务范围和模式进行约束时,实现必须将任务排队以启动异步事务。

一旦事务创建,就可以开始执行针对事务放置的请求,请求必须按照他们针对交易的顺序执行,他们的结果必须按照针对特定事务请求的顺序返回,无法保证不同的事务中请求的返回顺序。

事务模式保证不同事务的两个请求可以以任何顺序执行,而不会影响存储在数据库中的结果数据。

当处理与事务关联的每个请求时,将触发success 或 error 事件。在调度事件时,事务状态设置为active状态,允许针对事务发出其他请求。一旦事件派发完成,事务的状态将再次设置为inactive状态。

一个事务能够在结束前的任何时间点被终止,不管这个事务当前是active状态还是未开始状态。

当事务中止时(有错误发生),必须撤消(回滚)在该事务期间对数据库所做的任何更改。这包括对对象存储内容的更改以及对象存储和索引的添加和删除。

当针对事务的所有请求都已完成并处理其返回的结果、没有针对事务提出新请求且事务尚未中止时,实现必须尝试提交事务

事务开始之前,发起的这些请求不会执行,但是事务会跟踪记录这些请求的顺序。

就是说一个事务会创建一个事件循环队列,也会对应一个事件循环清除队列。

事务被成功commit后,complete事件会被触发。事务被abort时,abort事件会被触发。

2.7.2 事务调度

以下约束定义了何时开启一个事务:

  • 一个只读事务能够开始的条件:没有基于相同Object Store的读写事务在其之前创建且不处于finished状态(readonly事务是允许的)。

  • 一个读写事务能够开始的条件:没有基于相同Object Store的事务在其之前创建且不处于finished状态。

多个读写事务是阻塞式的顺序执行,多个只读事务可以同时执行。实际运行时会增加额外的约束,例如不允许并行运行作用域(Object Store)不重叠的事务,或者可能对运行事务的数量加以限制。

2.7.3 Upgrade Transactions

一个upgrade transaction 是mode是versionchange的事务。如果打开数据库时指定的版本大于当前版本,则在打开与数据库的连接后运行升级事务的步骤时,会自动创建upgrade transction,这个upgrade transction将会在upgradeneeded事件回调中处于激活状态。

upgrade transction的运行时完全排他的,直到基于该数据库的其他连接都被关闭,才会触发upgradeneeded事件,创建upgrade transction。这也确保了只要upgrade transction在运行状态,所有之前的事务都已完成,所有基于该数据库的连接都会被延迟,这期间创建的事务也会抛出异常。也就是当前正在运行的事务有且仅有upgrade transction。

2.8 requests

演示🌰(在线地址):

const DBOpenRequest = window.indexedDB.open("toDoList");
let db;

  // 当数据库打开出错/成功时,以下两个事件处理程序将分别对IDBDatabase对象进行下一步操作
  DBOpenRequest.onerror = function(event) {
    console.log(event, 'error------')
  };

  DBOpenRequest.onsuccess = function(event) {
    // 将打开数据库的结果存储在db变量中,该变量将在后面的代码中被频繁使用
    db = DBOpenRequest.result;

  };

  DBOpenRequest.onupgradeneeded = function(event) {
    var db1 = event.target.result;
    console.log(db1, 'db1----------')
    db1.onerror = function(event) {
      console.log(event, 'upgrade error---')
    };

    const objectStore = db1.createObjectStore("idbobjectStore", {  autoIncrement: true });
    // 创建索引
    const createReq1 = objectStore.createIndex("name", "name", { unique: false });
    createReq1.onsuccess = function() {};
    createReq1.onerror = function() {};
    
    const createReq2 = objectStore.createIndex("age", "age");
    createReq2.onsuccess = function() {};
    createReq2.onerror = function() {};
    
    // 组合索引
    const createReq3 = objectStore.createIndex("name+age", ["name", "age"]);
    createReq3.onsuccess = function() {};
    createReq3.onerror = function() {};
    
    // 数组索引
    const createReq4 = objectStore.createIndex("hobbies", "hobbies", {
      multiEntry: true
    });
    createReq4.onsuccess = function() {};
    createReq4.onerror = function() {};
    // 删除索引
    // const req = objectStore.deleteIndex("age");
    // req.onsuccess = function () {
    //   console.log('delete index success------');
    // }
    // req.onerror = function () {
    //   console.log('delete index error------');
    // }
  };

所有基于数据库的异步操作都是使用request,每个request代表一个操作。

一个request有一个processed flag 初始化为false,当请求执行被执行会被设置为true。

一个request有一个 done flag 初始化为false,当请求结果的拿到后会被设置为true

一个request有一个source object

一个request有resulterror,这些直到 done flag 为true的时候才可获得,分别存放请求success的结果和请求error的结果。

一个request的transaction初始化为null

一个request的parent属性返回的是这个请求的transaction,这会在基于特定事务发起请求时被设置。

请求通常不会被重用,但也有例外。迭代游标时,将在用于打开游标的同一请求对象上报告迭代成功。并且当升级事务是必要的时候,相同的打开请求用于upgradeneeded 事件和打开操作本身的最终结果。

2.8.1 open requests

除了success和error事件之外,还可以在打开请求时触发blocked和upgradeneeded事件以指示进度。

open request的source是null

open request的transction是nul,除非一个 upgradeneeded被触发。

同时期创建的多个open request会被放置进一个队列中,有序串行处理。

连接队列不是与事件循环关联的任务队列,因为请求是在任何特定浏览上下文之外处理的。向已完成的打开请求传递事件仍然通过与发出请求的上下文的事件循环相关联的任务队列。

2.9 Key range

  • key range具有与之关联的lower bound(null或者一个值)

  • key range具有与之关联的upper bound(null或者一个值)

  • key range具有与之关联的 lower open flag(是否包含下限,默认false包含)

  • key range具有与之关联的 upper open flag(是否包含上限,默认false包含)

  • key range的lower bound必须小于等于upper bound

一些特殊场景

  • lower bound等于upper bound,则相当于等于操作

  • lower bound是null,则相当于大于或者单于等于

  • upper bound是null,则相当于小于或者小于等于

一个无限的key range是lower bound和upper bound都是null,所有的keys都包含在无限的key range里。

2.10 Cursor 游标

游标用于在特定方向上迭代索引或对象存储中的一系列记录。

一个游标有一个事务,这个事务当游标创建时被激活。

游标在索引或者对象存储中具有一个范围内的记录。

游标有一个source标记哪个索引或对象存储与游标迭代的记录相关联。

一个游标的direction属性决定游标进行数据迭代的方向。该属性有以下取值:

  • next

  • nextunique

  • prev

  • prevunique

游标在其范围内有一个位置,在游标的整个范围被迭代执勤啊,游标正在迭代的记录列表可能会发生变化。为了处理这个问题,游标保持他们的位置不是使用index,而是使用先前返回记录的键,对于正向迭代的游标,下一次要求游标迭代到下一条记录时,它返回最小键大于先前返回记录的记录。对于反向迭代的游标,则返回最大键小于先前返回记录的记录。 对于迭代索引的游标,情况稍微复杂一些,因为多条记录可以具有相同的键,也按键值排序,当迭代索引时,游标还有一个对象存储位置,它指示索引中对应记录的值,查找下一条合适的记录会使用位置和对象存储位置。(这里的键:游标是基于索引创建则是索引值,否则是主键值)。

一个 Cursor有value标记,如果这个值为空,则Cursor要么正在加载下一个值,要么已经达到范围的末尾。当它有值时,表示游标当前指向一个值,并准备进入下一个值。

如果游标是基于整个Object Store创建的,则游标的有效数据源是Object Store,有效key是游标的position。如果游标是基于Object Store的某个索引创建的,则游标的有效数据源是该索引基于Object Store的引用对象存储,有效key不是游标的position,而是游标在Object Store中的position。

Cursor有一个request,这个request用于打开一个游标。

Cursor有一个key only标记,这决定是否可以访问到 Cursor的value属性。

2.11 key generators

当一个数据表未指明主键时,将会为其自动生成一个主键。

当key generator的当前数量超过值 2^53 (9007199254740992) 时,任何后续尝试使用key generator生成新值都将导致“ConstraintError”DOMException。仍然可以通过指定显式键将记录插入到对象存储中,但是再次为此类记录使用key generator的唯一方法是删除对象存储并创建一个新的。

此限制是因为大于 9007199254740992 的整数不能唯一地表示为 ECMAScript 数字。例如,ECMAScript 中的 9007199254740992 + 1 === 9007199254740992。

只要以正常方式使用 key generator,此限制就不会成为问题。如果你日夜每秒生成一个新值 1000 次,你将不会遇到这个限制超过 285000 年。

同样的key generator的值不会创建两次,除非发生事务回滚。

删除记录不会影响 key generator的递增,包括clear方法被调用。

store = db.createObjectStore("store1", { autoIncrement: true });
store.put("a"); // Will get key 1
store.delete(1);
store.put("b"); // Will get key 2
store.clear();
store.put("c"); // Will get key 3
store.delete(IDBKeyRange.lowerBound(0));
store.put("d"); // Will get key 4

你可以使用自定义的主键值(可以是浮点数,会自动处理成向下取整,可以是负数,也可以是字符串),这也不会影响key generator的递增。

若发生事务回滚, key generator将会重置计数器。

尝试在原始值上存储属性将失败并抛出错误。在下面的第一个示例中,对象存储的keypath是“foo”。实际对象是具有值 4 的基元。尝试在该基元值上定义属性失败。数组也是如此。数组上不允许使用属性。在下面的第二个示例中,实际对象是一个数组 [10]。尝试在数组上定义属性失败。

const store = db.createObjectStore("store", {
    keyPath: "foo",
    autoIncrement: true
});
// The key generation will attempt to create and store the key path
// property on this primitive.
store.put(4); // will throw DataError
// The key generation will attempt to create and store the key path// property on this array.
store.put([10]); // will throw DataError

3. Exceptions

DOMExceptions的异常类型和用法描述:

类型描述
AbortError请求被中断
ConstraintError由于未满足限制,事务的转换操作失败(例: 创建name重复的store)
DataCloneError内部结构化可控算法无法克隆正在存储的数据
DataError提供给操作的数据不符合要求
InvalidAccessError对对象执行了无效操作
InvalidStateError对不允许执行操作的对象或在不允许执行操作的时间调用操作,或者对已删除或已移除的源对象发起请求时调用操作。(例:创建store的时设置自增但指定的keyPath不合法)
NotFoundError操作失败,因为找不到请求的数据库对象(例:删除不存在的数据库时)
QuotaExceededError操作失败,没有足够的剩余存储空间,或者已达到存储配额并且用户拒绝为数据库提供更多空间
SyntaxErrorkeyPath参数包含了不合法的key path
ReadOnlyError在只读事务中尝试了变异操作(mutating 这个怎么更准确的翻译?)
TransactionInactiveError针对不在活动状态或者已完成的事务发起请求
UnknownError操作失败的原因与数据库本身无关,并且未包含在其他错误中
VersionError尝试使用低于现有版本的版本打开数据库
  • IndexDB的存储限制

    • 不能跨域

IndexedDB 的很多限制实际上与 Web Storage 一样。首先,IndexedDB 数据库是与页面源(协议、域 和端口)绑定的,因此信息不能跨域共享。这意味着 www.wrox.com 和 p2p.wrox.com 会对应不同的数据 存储。

  • 单个域名的存储空间限制

每个源都有可以存储的空间限制。当前 Firefox 的限制是每个源 50MB,而 Chrome 是 5MB。 移动版 Firefox 有 5MB 限制,如果用度超出配额则会请求用户许可。 Firefox 还有一个限制——本地文本不能访问 IndexedDB 数据库。Chrome 没有这个限制。因此若要在控制台做操作IndexDB数据库的测试,要使用 Chrome。

不要在索引属性里存储图片,视频或者巨大的字符串。大索引会影响数据库性能,在极端情况下会使其不稳定。

主键被隐式的标记为unique。索引对应存储的数据类型只能是字符串,数字,数组而不能是boolean, null 或者undeifned,对原来保存不可索引类型的值进行索引会不起作用。并且使用带有该属性的orderBy()也不会列出该对象。

鉴于多个indexed db数据库操作可能会引发相同类型的错误,并且即使单个操作也可能因为多种原因引发相同类型的错误,因此鼓励实现提供更具体的消息,以使开发人员能够识别错误原因。

4. API

API方法在不阻塞调用栈的情况下返回,所有的异步操作都会立即返回一个IDBRequest实例,此对象起初不包含有关操作结果的任何信息,一旦操作结果可获取,就会在请求上触发一个事件,并且该操作结果通过请求的相关属性可访问(result,error)。

4.1 IDBRequest

IDBRequest 接口提供了使用事件处理程序 IDL 属性 [HTML] 访问对数据库和数据库对象的异步请求结果的方法。

每个发起异步请求的方法都会返回一个IDBRequest对象,该对象通过事件与发出请求的应用程序进行通信。这种设计意味着同一时间可以针对任意数据库发起任意数量的请求。

IDBRequest实例的数据结构:

interface IDBRequest: EventTarget {
    readonly attribute any result;
    readonly attribute  DOMException? error;
    // 查询请求的触发源:Store or Index or IDBCursor
    readonly attribute (IDBObjectStore or IDBIndex or IDBCursor)?: source;
    // 关联事务
    readonly attribute IDBTransaction? transaction;
    // 请求状态
    readonly sttribute IDBRequestReadyState readyState;
    
    // success handler
    attribute EventHandler onsuccess;
    // error handler
    attribute EventHandler onerror;
}  
enum IDBRequestReadyState {
   "pending",
   "done"
}

IDBOpenDBRequest,open request创建的实例提供一个扩展的接口,允许监听blockedupgradeneeded 事件,IDBOpenDBRequest的数据结构如下:

interface IDBOpenDBRequest: IDBRequest {
    // Event handlers
    attribute EventHandler onblocked; // 事务被阻塞时触发
    attribute EventHandler onupgradeneeded; // 发生数据库版本升级时触发
}

一个 🌰(在线地址):

const request = indexedDB.open("IDBRequest");
request.onsuccess = function(event) {
  console.log(event, 'success-----')
}
request.onerror = function(event) {
  console.log(event, 'error-----')
}
request.onblocked = function(event) {
  console.log(event, 'error-----')
}
request.onupgradeneeded = function(event) {
   // 在这个upgrade transaction里可以创建数据表和管理数据表的索引
}
console.log(request, 'request----')

4.2 Event

此规范使用以下自定义接口触发事件:

interface IDBVersionChangeEvent : Event {
    constructor(
        DOMString type,
        optional IDBVersionChangeEventInit eventInitDict = {}
    );
    readonly attribute unsigned long long oldVersion;
    readonly attribute unsigned long long? newVersion;
 }
 
 dictionary IDBVersionChangeEventInit : EventInit {
  unsigned long long oldVersion = 0;
  unsigned long long? newVersion = null;
};

要在给定 oldVersion和newVersion的目标上触发version change event

示例🌰(在线地址):

// 触发一个version change event
var DBOpenRequest = window.indexedDB.open("eventInterface", 3);var db = null;

DBOpenRequest.onerror = function(event) {
   console.log(event, 'error')
};

DBOpenRequest.onsuccess = function(event) {
  db = DBOpenRequest.result;
};
DBOpenRequest.onupgradeneeded = function(event) {
    console.log(event,'------onupgradeneeded'); 
}

4.3 IDBFactory

通过IDBFactory 接口上的方法访问数据库对象,实现此接口的单个对象存在于indexed DB操作的全局范围内。

interface mixin WindowOrWorkerGlobalScope {
  // 总是返回相同对象的只读属性: indexedDB
  [SameObject] readonly attribute IDBFactory indexedDB;
};
// indexedDB属性为应用提供了一种访问indexed DB数据库功能的机制。
interface IDBFactory {
    // NewObject指示调用该方法的结果总是创建一个新的对象
    // 打开数据库,创建基于数据库「name」的连接
    [NewObject] IDBOpenDBRequest open(DOMString name,
                                    optional [EnforceRange] unsigned long long version);
    // 删除
    [NewObject] IDBOpenDBRequest deleteDatabase(DOMString name);
    
    Promise<sequence<IDBDatabaseInfo>> databases();
    // -1: key1 < key2, 1: key1 > key2, 0: key1 = key2
    short cmp(any key1, any key2);
}
dictionary IDBDatabaseInfo {
  DOMString name;
  unsigned long long version;
};

关于 result = indexedDB . ``cmp``(key1, key2),比较两个值的步骤:

  1. 如果key1、key2不是合法DOMString,则抛出错误。

  2. key1、key2 遵循以下比较规则:

    1. ta 表示 typeof a

    2. tb 表示 typeof b

    3. 如果ta 不等于 tb,则执行以下步骤:

      1. ta 是 array,return 1
      2. tb 是 array,return -1
      3. ta 是 binary,return 1
      4. tb 是 binary,return -1
      5. ta 是 string,return 1
      6. tb 是 string,return -1
      7. ta 是 date,return 1
      8. tb 是 date,return -1
    4. va 表示 value of a,vb 表示 value of b

    5. switch on ta

      1. number & date
      2.     1: va > vb, -1: va < vb, 0: va = vb;
      3. string
      4.     1: va code unit > vb, -1: va code unit < vb, 0: a code unit = vb
      5. binary
      6.     1: va byte less than vb, -1: va byte granter than vb,0: otherwise
      7.     二进制数据的比较范围是无符号数:0-255
      8. array
      9.     长度相同,则逐一比较;长度不同,则数组数量多的大。
    6. 由于上述规则,-infinity 是键的最小值。 数字小于日期。 日期小于字符串。 字符串小于二进制。 二进制小于数组。通常将 [[]] 作为数据库存储的最大值。 没有最高可能的键值。 这是因为任何候选最高键后面跟着另一个键的数组甚至更高。

4.4 IDBDatabse 实例

IDBDatabse 对象在于数据库创建连接后生成,如果IDBDatabse的关联连接的close pending flag标记为false,并且具有一个或多个事件监听器,其类型为abort,error, versionchange,则不得对IDBDatabse进行垃圾收集。如果IDBDatabse对象被垃圾回收,则必须关闭数据库连接。

数据结构:

interface IDBDatabase: EventTarget {
    readonly attribute DOMString name;
    // 标记当前连接对应的数据库版本,不会随着数据库升级而更新
    readonly attribute unsigned long long version;
    // 当前数据库下的所有数据表,按store name排序
    readonly attribute DOMStringList objectStoreNames;
    //可以理解为构造函数每次都返回新对象
    [NewObject] IDBTransaction transaction(
                        (DOMString or sequence<DOMString>) storeNames,
                        optional IDBTransactionMode mode = "readonly",
                        optional IDBTransactionOptions options={});
    undefined close();
    // 创建数据库,只能在upgrade transaction里被调用
    // 否则会抛出InvalidStateError
    [NewObject] IDBObjectStore createObjectStore(
        DOMString name,
        optional IDBObjectStoreParameters options = {};
    );
    // 删除数据库,只能在upgrade transaction里被调用
    // 否则会抛出InvalidStateError
    undefined deleteObjectStore(DOMString name);
    
    // Event handlers
    attribute EventHandler onabort;
    // 关闭操作会等待所有未完成的事务完成后再执行
    attribute EventHandler onclose;
    attribute EventHandler onerror;
    attribute EventHandler onversionchange;
}
// 事务的持久性
enum IDBTransactionDurability { "default" , "strict" , "relaxed" };
dictionary IDBTransactionOptions {
    IDBTransactionDurability durability = "default";
}
interface IDBObjectStoreParameters {
    keyPath?: DOMString; // default null
    autoIncrement: boolean; // default false
}

示例🌰(在线地址):

 var DBOpenRequest = window.indexedDB.open("IDBDatabaseTest");
 var db;

  // 当数据库打开出错/成功时,以下两个事件处理程序将分别对IDBDatabase对象进行下一步操作
  DBOpenRequest.onerror = function(event) {
    console.log(event, 'error--------')
  };

  DBOpenRequest.onsuccess = function(event) {
    // 将打开数据库的结果存储在db变量中,该变量将在后面的代码中被频繁使用
    db = DBOpenRequest.result;

  };

  DBOpenRequest.onupgradeneeded = function(event) {
    var db1 = event.target.result;

    db1.onerror = function(event) {
      // console.log(event, 'upgrade error---')
    };

    // 使用IDBDatabase.createObjectStore方法,可创建一个对象存储区
    var objectStore = db1.createObjectStore("toDoList", { keyPath: "taskTitle"       });

    // 定义objectStore将包含哪些数据项

    objectStore.createIndex("hours", "hours", { unique: false });
    objectStore.createIndex("minutes", "minutes", { unique: false });
    
    // deleteIndex 也会在此回调中调用,用于处理版本差异
    
    var objectStore1 = db1.createObjectStore("abb");
    objectStore1.name = "aaa";
    
    var objectStore2 = db1.createObjectStore("ddd", {
      autoIncrement: true
    });
    // db1.deleteObjectStore("abb");
    // 删除不存在的Store报错会抛出 Uncaught DOMException  
    // db1.deleteObjectStore("ewfweef");
    
  };
setTimeout(() => {
  const transaction = db.transaction(["ddd", "toDoList"], "readwrite");
  // 如果创建的事务没有指定相应的scope,基于它做数据库访问会报错
  const objStore = transaction.objectStore("ddd");
  // 插入数据
    const resquest = objStore.add("wwww")
    resquest.onsuccess = function(event) {
       // console.log(event.target.result, 'put value---')
    }
    resquest.onerror = function(err) {
      // console.log(err)
    }
  objStore.add("6666", 2)
  objStore.add("8888", "uuu")
  
  // add key 已存在的数据会报错
  // const req = objStore.add("777", "uuu");
  // req.onsuccess = function(event) {
  //   console.log(event, 'evnt------');  
  // }
  // req.onerror = function(err) {
  //   console.log(err);
  // }
  
  // 更新一个存在的值
  // objStore.put("1118", "uuu")
  
  // 更新一个key不存在的值会帮你执行put操作     
  // objStore.put("000", "llll")
  
  // 删除一个存在的值
  // const deleteReq = objStore.delete("uuu")
  //  deleteReq.onsuccess = function(event) {
  //   console.log(event, 'delete success------');  
  // }
  // deleteReq.onerror = function(err) {
  //   console.log(err, 'delete error------');
  // }
  
  
  // 删除一个不存在的值也能成功
  // const deleteReq1 = objStore.delete("444")
  //  deleteReq1.onsuccess = function(event) {
  //   console.log(event, 'delete success------1');  
  // }
  // deleteReq1.onerror = function(err) {
  //   console.log(err, 'delete error------1');
  // }
  
}, 1000)

4.5 IDBObjectStore 接口

IDBObjectStore 是一个object store的实例。

数据结构:

interface IDBObjectStore {
    // 只能在upgrade transaction里设置name
    attribute DOMString name;
    // 主键
    readonly attribute any KeyPath;
    // 索引列表,排序后的
    readonly attribute DOMStringList indexNames;
    // 与该Store实例关联的transtion实例
    [SameObject] readonly attribute IDBTransaction transation;
    // 如果为true,则该store会在插入数据时会自动生成自增的主键
    readonly attribute boolean autoIncrement;
    
    // 如果未设置自增,启用key generator,则第二个参数是必填
    // 关联transaction mode是readwrite,成功返回记录的主键值 
    [NewObject] IDBRequest put(any value, optional any key); 
    // 关联transaction mode是readwrite, 成功返回记录的主键值 
    [NewObject] IDBRequest add(any value, optional any key);
    // 针对主键查询记录并删除,成功返回undefined
    // 参数不允许传空,以防止不小心执行clear操作
    [NewObject] IDBRequest delete(any query);
    // 清除store,成功返回undefined
    [NewObject] IDBRequest clear();
    [NewObject] IDBRequest get(any query);
    // 返回查询记录的主键key
    [NewObject] IDBRequest getKey(any query);
    // 返回查询结果命中的所有记录,且可以限制长度
    [NewObject] IDBRequest getAll(optional any query,
                                  optional [EnforceRange] unsigned long count);
    // 返回query命中的记录数
    [NewObject] IDBRequest count(optional any query);
    // 创建基于query命中结果的游标
    [NewObject] IDBRequest openCursor(optional any query,
                                    optional IDBCursorDirection direction = "next");
    // 创建基于query命中结果的key的吗 游标
    [NewObject] IDBRequest openKeyCursor(optional any query,
                                    optional IDBCursorDirection direction = "next");
    // 获取store的对应索引实例
    IDBIndex index(DOMString name); 
    // 创建索引
    [NewObject] IDBIndex createIndex(DOMString name,
                                     DOMString keyPath,
                                     optional IDBIndexParameters options = {});
    // 删除索引
    undefined deleteIndex(DOMString name);                                                                                             
                                               
}
dictionary IDBIndexParameters {
    boolean unque = false;
    boolean multiEntry = false;
}

示例🌰(在线地址):

 var DBOpenRequest = window.indexedDB.open("IDBDatabaseTest");
 var db;

  // 当数据库打开出错/成功时,以下两个事件处理程序将分别对IDBDatabase对象进行下一步操作
  DBOpenRequest.onerror = function(event) {
    console.log(event, 'error--------')
  };

  DBOpenRequest.onsuccess = function(event) {
    // 将打开数据库的结果存储在db变量中,该变量将在后面的代码中被频繁使用
    db = DBOpenRequest.result;

  };

  DBOpenRequest.onupgradeneeded = function(event) {
    var db1 = event.target.result;

    db1.onerror = function(event) {
      // console.log(event, 'upgrade error---')
    };

    // 使用IDBDatabase.createObjectStore方法,可创建一个对象存储区
    var objectStore = db1.createObjectStore("toDoList", { keyPath: "taskTitle"       });

    // 定义objectStore将包含哪些数据项

    objectStore.createIndex("hours", "hours", { unique: false });
    objectStore.createIndex("minutes", "minutes", { unique: false });
    
    // deleteIndex 也会在此回调中调用,用于处理版本差异
    
    var objectStore1 = db1.createObjectStore("abb");
    objectStore1.name = "aaa";
    
    var objectStore2 = db1.createObjectStore("ddd", {
      autoIncrement: true
    });
    // db1.deleteObjectStore("abb");
    // 删除不存在的Store报错会抛出 Uncaught DOMException  
    // db1.deleteObjectStore("ewfweef");
    
  };
setTimeout(() => {
  const transaction = db.transaction(["ddd", "toDoList"], "readwrite");
  // 如果创建的事务没有指定相应的scope,基于它做数据库访问会报错
  const objStore = transaction.objectStore("ddd");
  // 插入数据
    const resquest = objStore.add("wwww")
    resquest.onsuccess = function(event) {
       // console.log(event.target.result, 'put value---')
    }
    resquest.onerror = function(err) {
      // console.log(err)
    }
  objStore.add("6666", 2)
  objStore.add("8888", "uuu")
  
  // add key 已存在的数据会报错
  // const req = objStore.add("777", "uuu");
  // req.onsuccess = function(event) {
  //   console.log(event, 'evnt------');  
  // }
  // req.onerror = function(err) {
  //   console.log(err);
  // }
  
  // 更新一个存在的值
  // objStore.put("1118", "uuu")
  
  // 更新一个key不存在的值会帮你执行put操作     
  // objStore.put("000", "llll")
  
  // 删除一个存在的值
  // const deleteReq = objStore.delete("uuu")
  //  deleteReq.onsuccess = function(event) {
  //   console.log(event, 'delete success------');  
  // }
  // deleteReq.onerror = function(err) {
  //   console.log(err, 'delete error------');
  // }
  
  
  // 删除一个不存在的值也能成功
  // const deleteReq1 = objStore.delete("444")
  //  deleteReq1.onsuccess = function(event) {
  //   console.log(event, 'delete success------1');  
  // }
  // deleteReq1.onerror = function(err) {
  //   console.log(err, 'delete error------1');
  // }
  
}, 1000)

4.6 IDBIndex

IDBIndex 索引

数据结构:

interface IDBIndex {
  // 索引的名称(别名),支持在upgrade transaction里通过 .name = xxx 的方式更新
  attribute DOMString name;
  // 索引所属的object store实例
  [SameObject] readonly attribute IDBObjectStore objectStore;
  // 索引对应的keyPath
  readonly attribute any keyPath;
  // 数组索引标记
  readonly attribute boolean multiEntry;
  // 唯一性标记
  readonly attribute boolean unique;
  
  // 返回query命中的第一条记录
  [NewObject] IDBRequest get(any query);
  // 返回query命中的第一条记录的key
  [NewObject] IDBRequest getKey(any query);
  // 返回query命中的前count个记录
  [NewObject] IDBRequest getAll(optional any query,
                                optional [EnforceRange] unsigned long count);
  // 返回query命中的前count个记录的key                             
  [NewObject] IDBRequest getAllKeys(optional any query,
                                    optional [EnforceRange] unsigned long count);
  // 返回query命中的统计
  [NewObject] IDBRequest count(optional any query);
  
  // 创建游标,可通过 cursor.value 获取游标指向的记录值
  [NewObject] IDBRequest openCursor(optional any query,
                                    optional IDBCursorDirection direction = "next");
  // 创建游标,只能通过 cursor.key 获取游标的 主键值 or 索引值
  [NewObject] IDBRequest openKeyCursor(optional any query,
                                       optional IDBCursorDirection direction = "next");
};

示例🌰(在线地址):

const DBOpenRequest = window.indexedDB.open("CursorTestDB");
let db;

  // 当数据库打开出错/成功时,以下两个事件处理程序将分别对IDBDatabase对象进行下一步操作
  DBOpenRequest.onerror = function(event) {
    console.log(event, 'error------')
  };

  DBOpenRequest.onsuccess = function(event) {
    // 将打开数据库的结果存储在db变量中,该变量将在后面的代码中被频繁使用
    db = DBOpenRequest.result;

  };

  DBOpenRequest.onupgradeneeded = function(event) {
    var db1 = event.target.result;
    console.log(db1, 'db1----------')
    db1.onerror = function(event) {
      console.log(event, 'upgrade error---')
    };

    const objectStore = db1.createObjectStore("idbobjectStore", {  autoIncrement: true });
    // 创建索引
    const createReq1 = objectStore.createIndex("name", "name", { unique: false });
    createReq1.onsuccess = function() {};
    createReq1.onerror = function() {};
    
    const createReq2 = objectStore.createIndex("age", "age");
    createReq2.onsuccess = function() {};
    createReq2.onerror = function() {};
    
    // 组合索引
    const createReq3 = objectStore.createIndex("name+age", ["name", "age"]);
    createReq3.onsuccess = function() {};
    createReq3.onerror = function() {};
    
    // 数组索引
    const createReq4 = objectStore.createIndex("hobbies", "hobbies", {
      multiEntry: true
    });
    createReq4.onsuccess = function() {};
    createReq4.onerror = function() {};
    // 删除索引
    // const req = objectStore.deleteIndex("age");
    // req.onsuccess = function () {
    //   console.log('delete index success------');
    // }
    // req.onerror = function () {
    //   console.log('delete index error------');
    // }
  };

4.7 IDBKeyRange

IDBKeyRange 主要用于创建一个索引范围

数据结构:

interface IDBKeyRange {
  // 下界
  readonly attribute any lower;
  // 上界
  readonly attribute any upper;
  // 是否包含下界
  readonly attribute boolean lowerOpen;
  // 是否包含上界
  readonly attribute boolean upperOpen;

  // Static construction methods:
  // 等于
  [NewObject] static IDBKeyRange only(any value);
  // 小于 | 小于等于
  [NewObject] static IDBKeyRange lowerBound(any lower, optional boolean open = false);
  // 大于 | 大于等于
  [NewObject] static IDBKeyRange upperBound(any upper, optional boolean open = false);
  // 在一个范围之间
  [NewObject] static IDBKeyRange bound(any lower,
                                       any upper,
                                       optional boolean lowerOpen = false,
                                       optional boolean upperOpen = false);

  // 查询key值是否在range范围内
  boolean includes(any key);
};

4.8 IDBCursor

Cursor objects是IDBCursor的实例,只有一个IDBCursor代表一个给定的cursor,可以同时使用多少cursor没有限制。

数据结构:

interface IDBCursor {
    // 游标绑定的IDBObjectStore 或 IDBIndex
    readonly attribute (IDBObjectStore or IDBIndex) source;
    readonly attribute IDBCursorDirection direction;
    // 当前游标指向数据的 主键值或索引值
    readonly attribute any key;
    // 当前游标指向数据的主键值
    readonly attribute any primaryKey;
    [SameObject] readonly attribute IDBRequest request;
 
    undefined advance([EnforeRange] unsigned long count);
    // 跳转到下一个匹配索引值 或 主键值 的记录
    // 若key比当前记录值小,且方向是next,则抛错
    // 若key比当前记录值大,且方向是prev,则抛错
    undefined continue(optional any key);
    // 当前游标是索引游标,跳转到下一个匹配 主键值的记录
    undefined continuePrimaryKey(any key, any primaryKey);
   
    // 更新当前游标指向记录,如果该游标基于readonly的transaction会报错
    [NewObject] IDBRequest update(any value);
    // 删除当前游标指向记录,如果该游标基于readonly的transaction会报错
    [NewObject] IDBRequest delete()
};

// Cursor的key only为false则会创建IDBCursorWithVallue实例
interface IDBCursorWithVallue: IDBCursor {
    readonly attribute any value;
}

enum IDBCursorDirection {
    "next",
    "nextunique",
    "prev",
    "prevunique"
}

4.9 IDBTransaction

事务对象是IDBTransaction的实例

数据结构:

interface IDBTransaction : EventTarget {

  // 事务创建时关联的Store,当前事务的作用域内可操作的object store’s name list
  readonly attribute DOMStringList objectStoreNames;
  // 事务的模式
  readonly attribute IDBTransactionMode mode;
  // 事务的持久性能力,新特性,兼容性要注意 
  readonly attribute IDBTransactionDurability durability;
  // 事务关联的数据库连接实例
  [SameObject] readonly attribute IDBDatabase db;
  // 事务发生错误时该属性会被赋值
  readonly attribute DOMException? error;
  
  // 获取该事务作用范围内的object store实例
  IDBObjectStore objectStore(DOMString name);
  // 提交事务,手动触发事务的完成
  undefined commit();
  // 终止事务,手动触发事务的终止
  undefined abort();

  // Event handlers:
  // 事务被阻断,基于数据库的相关操作会被回滚
  attribute EventHandler onabort;
  // 可以更好的反应一个事务是否成功,因为事务仍会fail在onsuccess被触发之后
  attribute EventHandler oncomplete;
  attribute EventHandler onerror;
};

enum IDBTransactionMode {
  "readonly" ,
  "readwrite" ,
  "versionchange"
};
  1. durability

    1.   事务具有持久性提示,这提示用户代理在提交事务时是否优先考虑性能或持久性,持久性提示有以下取值:
    2. strict
    3.   只有在验证所有未完成的更改都已经成功写入持久存储介质后,用户代理才认为事务已成功提交。
    4. relaxed
    5.   一旦所有未完成的操作都已经写入操作系统,用代理就会认为事务已成功提交,无需后续验证。
    6. default
    7.   用户代理使用storage bucket默认的持久性行为。
    8.   strict模式会花费大量时间并消耗便携式设备的电池寿命,鼓励web应用程序对临时数据使用relaxed,并在降低数据丢失风险超过对性能和功率的影响的情况下使用strict,使用者需要权衡用户程序的持久性能力对用户和用户设备的影响。
  2. objectStore(storeName)

同样的transaction,storeName会返回同样的IDBObjectStore实例;

不同事务创建的同一个storeName 会返回不同的IDBObjectStore 实例;

  1. commit()

新特性,需考虑兼容性问题

尝试提交事务,所有待处理的事务都将被允许完成,但不会接收新的请求。这可用于强制事务快速完成,无需等待挂起的请求,再尝试正常提交之前触发成功事件。

如果挂起的请求失败,如约束错误,事务将终止,成功请求的事件仍将触发,但在onsuccess的回调中抛出异常不会终止事务,同样,失败请求的错误回调仍会触发,调用preventDefault()不会阻止事务终止。

通常不需要主动触发commit,当所有未完成的请求完成,且没有新的请求提出后,commit会自动触发。此操作可以用于启动commit,无需等待未完成的请求触发。

5. Algorithms(我这里理解为操作)

5.1 打开数据库

创建数据库连接的步骤:

  1. 查找当前域名的connection queue,添加request到队列中

  2. 等待队列中前面的请求完成

  3. 创建对应数据库名称的连接,若version不存在,则初始化version为1

  4. 访问的数据库不存在,则新建,初始化version为0,若创建失败,则会抛出一个有关的error

  5. 若数据库版本大于指定的version,则抛出VersionError

  6. 创建connectionconnection.version = version

  7. 如果db的version小于指定的version

    1. 创建与db相关联的所有连接的集合openConnections;
    2. 给每一个close pending flag没有设置为true的open connection,入队列一个触发db的version到指定version的versionchange事件的任务;
    3. 若基于当前db的所有open connectons未关闭,则排队一个任务触发名为block的versionchange事件。
    4. 直到所有的open connections都关闭
    5. 运行upgrade transaction
    6. 如果连接已关闭,则抛出AbortError并终止这些步骤
    7. return connection

5.2 关闭数据库

使用connection object来关闭数据库连接,操作如下:

  1. 设置connectionclose pending flagtrue

此情况下不允许基于该连接创建新的事务,所有的创建事务的方法都会先检查conntion的close pending flag是否为true,如果是则抛错。

  1. 如果forced flag设置为true,则终止基于此连接创建的事务并抛出AbortError。

  2. 等待所有基于此连接创建的事务完成,一旦完成,连接就会关闭。

  3. 如果forced flag设置为true,则触发一个close事件。

close事件监听会在数据库未正常关闭的情况下触发,如果域名下的storage被清除,或者系统异常,或者I/O错误会触发。如果正常调用close关闭,则不会触发close事件监听。

若要删除数据库,则会等待基于该数据库的所有connection都关闭。

5.3 删除数据库

删除数据库的操作如下:

  1. 找到当前域名下,「name」对应的数据库的connection queue

  2. 创建请求添加进队列

  3. 等待队列中该请求前面的请求都开始

  4. 赋值「name」数据库的实例给db

  5. openConnections为与db关联的所有连接的集合

  6. 对所有为将其close pending flag设置为true的连接,创建一个触发version change的任务进队列入口

触发version change事件可能会导致openConnections中的一个或多个连接被关闭,这种情况下,不会对这些连接触发versionchange事件,即使未完成也是如此。

  1. 等待直到所有事件被触发

  2. 如果openConnections仍有连接未关闭,则入队一个名为blockrequest来触发versionchange事件。

  3. 等待直到所有的openConnections被关闭

  4. 删除数据库,如果出错,则返回相关联的error( "QuotaExceededError" or "UnknownError")

  5. return 数据库对应的版本号;

5.4 提交事务

提交事务的操作如下:

  1. 设置transactionstatecommiting

  2. 并行运行如下步骤:

    1. 等待知道transaction的请求列表里的所有请求开始执行

    2. 当所有事务的状态不在committing,则终止步骤

    3. 考虑到事务的持久性提示,尝试将事务所做的任何未完成的更改写入数据库

    4. 如果更改写入数据库时发生错误,则使用事务和适当的错误类型(例如“QuotaExceededError”或“UnknownError”DOMException)运行中止事务,并终止这些步骤。

    5. 入队列一个任务执行如下步骤:

      1. 如果事务是一个upgrade transaction,则设置事务的关联数据库的ugrade transation的连接设置为null
      2. 设置transaction的state为finished
      3. 释放事务的complete事件
      4.     即使从事该事务的处理程序之一抛出异常,事务仍会提交,因为写入数据库更改发生在事件发生之前,只有在事务成功写入后才会触发complete事件。
      5. 如果事务是一个upgrade transaction,则创建一个与该事务关联的请求,然后设置请求的transaction为null。

5.5 终止事务

想要终止事务,运行以下步骤:

  1. 事务被终止后,事务所做的任何更改操作都将被还原,这也包括upgrade transaction创建的object store和索引。

  2. 如果要终止的事务是upgrade transaction,运行以下步骤来终止:

这将会还原与该事务相关联的所有连接,object store和索引的更改。

  1. 设置事务的状态为finished

  2. 如果终止遇到error,则设置transactionerror属性为error

  3. 终止基于当前事务创建的所有请求的执行,设置请求的processed flag为true,执行以下步骤入队列。

    1. 设置request的done falgtrue
    2. 设置request的resultundefined
    3. 设置request的error为创建的AbortError
    4. 触发一个名为error的事件,初始化bubblescancelable属性为true
    5.   这并不总是导致error的发生,例如:如果事务在committing时由于错误而终止,或者他是最后一个是剩余请求失败了。
  4. 入队一个任务执行以下步骤:

    1. 如果事务是upgrade transaction,则设置事务的connections关联的upgrade transaction为null。

    2. 在事务中触发一个名为abort的事件,设置它的bubbles属性为true

    3. transactionupgrade transaction

      1. 让请求成为一个与事物关联的打开数据库连接的请求
      2. 设置requesttransaction为null
      3. request.result = undefined
      4. request.proccedded = false
      5. request.done = false

5.6 异步触发一个请求

如果使用了终止事务终止了创建的请求所属的事务,即可以随时终止这些步骤。开始执行一个异步请求有以下步骤:

  1. 创建与source关联的事务

  2. transactionstate设置为active

  3. 如果请求未创建,则创建一个基于source的新请求

  4. 将请求添加进事务的request list

  5. 并行运行以下步骤:

    1. 等待直到trancationrequest list中的第一项是未开始的

    2. 赋值result为执行操作的结果

    3. 若请求的结果是error,并且事务的状态正在提交,则运行终止事务和result来终止这些步骤

    4. 如果resulterror,则会退操作的所有更改

    5.   注意会退的是当前requestchange,而不是基于当前事务的所有request的change

    6. request.processed = true

    7. 入队一个任务执行以下步骤:

      1. 从transaction的request list移除当前request

      2. request.done = true

      3. 如果请求的结果是一个error,则

        1.       request.reqult = undefined;
        2.       rrquest.error = result;
        3.       触发request的onerror事件
      4. 否则

        1.       request.reqult = result;
        2.       rrquest.error = undefined;
        3.       触发request的onsuccess事件
  6. return request

5.7 运行upgrade transaction

使用被用来更新数据库的连接来运行upgrade transaction,给数据库设置一个新的版本,创建一个request,运行以下步骤:

  1. 定义db为数据库的连接

  2. 定义transaction为一个与connection关联的新的upgrade transaction

  3. 设置transaction的scope为object store实例

  4. 设置db的upgrade transaction为transaction

  5. 设置事务的状态为inactive

  6. 运行事务

注意:在当前事务执行完成前,都不能建立与当前数据库的连接

  1. 更新数据库版本

  2. 更新数据库版本的操作也会被认为是事务的一部分,所以如果事务终止发生回滚,这一操作也会回滚

  3. request.processed = true

  4. 入队列一个任务执行以下步骤:

    1. request.result = connection
    2. request.transaction = transaction
    3. request.done = true
    4. transaction.state = active
    5. 触发一个名为 upgradeneeded 的version change事件,让didThrow为事件的result
    6. transaction.state= inactive
    7. 如果didThrow是true,则使用事务和新创建的AbortError DOMException来终止事务
  5. 等待事务执行完毕

5.8 终止upgrade transaction

用事务来终止一个upgrade transaction,这会发生一些与其关联的object store和索引改动的回退操作,运行以下步骤:

  1. 创建事务的连接connection

  2. connection.database = database

  3. 设置connection的version为database的version,如果database是新建的,则设置为0

  4. 设置connection的objectStoreNames如果数据库已存在,数据库是新建的则设置为空数组

  5. 创建每一个object store与transaction的关联,包括在事务中创建和删除的object store和object store的索引

5.9 触发success event

通过执行以下步骤,来触发success event:

  1. 创建event为Event的实例

  2. 设置event的type为success

  3. 设置event的bubbles和cancelable为false

  4. 创建一个请求的transaction

  5. legacyOutputDidListenersThrowFlag 初始化为false

  6. 激活事务,设置transaction的state为active

  7. 使用legacyOutputDidListenersThrowFlag派发事件请求

  8. 当事务执行完成,执行:

    1. 设置事务的状态为inactive
    2. 如果legacyOutputDidListenersThrowFlagweitrue,则运行事务来终止事务,并抛出AbortErroe
    3. 如果transaction的request list是空的,则提交事务

5.10 触发error event

error event的触发步骤和success event的触发步骤大致相同,有区别的是,error event被触发时,该错误会被冒泡到request关联的transaction上,事务被终止,transaction.error会被设置为AbortError。

5.11 Clone a value

要在事务期间在 targetRealm 中克隆值,请运行以下步骤:

  1. 如果transaction的state为active

  2. 设置transaction的state为inactive

让事务处于inactive状态以保证由克隆操作触发的getter或其他副作用无法针对事务发起请求

  1. let serialized = StructuredSerializeForStorage (value);

  2. let clone = StructuredDeserialize(serialized, targetRealm)

  3. 设置transaction的state为active

  4. return clone

6. 数据库操作

本节将介绍对数据库中的object store和indexes进行的各种操作,这些操作由异步请求的步骤运行,这部分内容主要是基于IndexDB的一些列的数据库操作。

6.1 object store存储操作

对于数组索引,插入空数组是合法的,但是这条记录不会被添加到索引记录中。

6.2 Object Store检索操作

Object Store的数据存取操作:插入,修改,查询,删除等。

6.3 索引检索操作

为了更快速的进行数据的读取,可以为数据库创建索引,这样使得可以基于索引值查询Object Store中存储的数据。

6.4 Object Store删除操作

可以删除 Object Store中存储的所有记录或者查询结果命中的那些记录

6.5 count 操作

获取Object Store中存储的所有记录或者查询结果命中的那些记录的数量

6.6 Clear操作

清空某个Object Store中存储的所有记录

6.7 Cursor查询

实际的实现会将数据持久化到非易失性存储介质中。数据在存储时将被序列化,在检索时将被反序列化,尽管序列化格式的细节将是特定于用户代理的。用户代理可能会随着时间的推移改变其序列化格式。例如,可以更新格式以处理新的数据类型或提高性能。为了满足本规范的操作要求,实现必须以某种方式处理旧的序列化格式。对旧数据的不当处理可能会导致安全问题。除了基本的序列化问题之外,序列化的数据还可以编码在较新版本的用户代理中无效的假设。

一个实际的例子是 RegExp 类型。 StructuredSerializeForStorage 操作允许序列化 RegExp 对象。典型的用户代理会将正则表达式编译为本机机器指令,并假设输入数据如何传递和结果返回。如果这个内部状态被序列化为存储到数据库中的数据的一部分,那么当内部表示后来被反序列化时可能会出现各种问题。例如,将数据传递到代码中的方式可能已经改变。编译器输出中的安全错误可能已在用户代理的更新中被识别和修复,但仍处于序列化的内部状态。

用户代理必须正确识别和处理旧数据。一种方法是在序列化格式中包含版本标识符,并在遇到旧数据时从脚本可见状态重建任何内部状态。

Q&A

希望通过一些思考题,来加深本次的学习

  1. 如何通过范围查找索引值在[a, b)之间的数据?

  2. 可以创建 a.b这样的索引吗?

  3. 数组索引的查询支持in操作吗?

  4. 组合索引的查询,如果只想查询组合索引中的部分字段能实现吗?

揭晓时刻(在线地址):

注:demo的内容会有点多,集中了多个使用场景(涵盖了非常多游标查询的示例),我都有对应的注释,大家可以根据注释的引导查看对应的功能示例(^_^)。

const DBOpenRequest = window.indexedDB.open("qustionsdb");
let db;

  // 当数据库打开出错/成功时,以下两个事件处理程序将分别对IDBDatabase对象进行下一步操作
  DBOpenRequest.onerror = function(event) {
    console.log(event, 'error------')
  };

  DBOpenRequest.onsuccess = function(event) {
    // 将打开数据库的结果存储在db变量中,该变量将在后面的代码中被频繁使用
    db = DBOpenRequest.result;

  };

  DBOpenRequest.onupgradeneeded = function(event) {
    var db1 = event.target.result;
    console.log(db1, 'db1----------')
    db1.onerror = function(event) {
      console.log(event, 'upgrade error---')
    };

    const objectStore = db1.createObjectStore("qustionsStore", {  autoIncrement: true });
    // 创建索引
    objectStore.createIndex("name", "name", { unique: false });
    objectStore.createIndex("age", "age");
    
    // 组合索引
    objectStore.createIndex("name_age", ["name", "age"]);

    objectStore.createIndex("name_age_height", ["name", "age", "height"]);
    
    // 嵌套索引
    objectStore.createIndex("a/b", "a.b");

    
    // 数组索引
    objectStore.createIndex("hobbies", "hobbies", {
      multiEntry: true
    });
  };

const addData = async (objectStore, data) => {
  const promises = [];
  promises.push(new Promise((resolve, reject) => {
    data.forEach(item => {
       const request = objectStore.add(item);
       request.onsuccess = function(event) {
         // console.log(event, 'add success-----')
         resolve(request.returnValue);
       }
       request.onerror = function(event) {
         // console.log(event, 'add error-----')
         reject(request.returnValue);
       }
    })
     
  }))
  return Promise.all(promises);
}

// objectStore: IDBObjectStore or IDBIndex
const getData = async (objectStore, query, onlyKey = false, num) => {
  return new Promise((resolve, reject) => {
    let request = null;
    if(num === 1) {
      request = onlyKey ? objectStore.getKey(query) : objectStore.get(query);
    } else {
      request = onlyKey ? objectStore.getAllKeys(query, num) : objectStore.getAll(query, num);
    }
    console.log(request.readyState, 'query init-----')
    request.onsuccess = function(event) {
      console.log(request, 'query success-----', request.parent)
      request.readyState = 'pending'
      resolve(request.result);
    }
    request.onerror = function(event) {
      // console.log(event, 'query error-----')
      reject(request.result);
    }
  })
}

const getCount = async (objectStore, query) => {
  return new Promise((resolve, reject) => {
    const request = objectStore.count(query);
    request.onsuccess = function(event) {
      console.log(event, request, 'count success-----')
      resolve(request.result);
    }
    request.onerror = function(event) {
      // console.log(event, 'query error-----')
      reject(request.result);
    }
  })
}

const dataGenerator = (num) => {
  let arr = []
  for(let i = 0; i < num; i++) {
    arr.push({
      name: 'zhangsan' + i,
      age: i,
      height: '170' + i,
      hobbies: ["112"+i, "444"+i, '999'+i],
      a: {
        b: "bbb" + i
      }
    })
  }
  return arr;
}

setTimeout(async () => {
  // 数据库连接实例
  // console.log(DBOpenRequest, 'DBOpenRequest-----')
  const transaction = db.transaction("qustionsStore", "readwrite");

  const objStore1 = transaction.objectStore("qustionsStore");
  
  // 批量插入数据
  // await addData(objStore1, dataGenerator(100));
  
  
// question one: 查询[a,b) 之间的数据
// const keyRangeValue = IDBKeyRange.bound(470, 500, false, true);
// const res2 = await getData(objStore1, keyRangeValue, false);
// console.log(res2, 'res2-----');
  
// question two: 基于a.b的数据查询
// const nestedIndex = objStore1.index("a/b");
// const res3 = await getData(nestedIndex, "bbb2", false, 1);
// console.log(res3, 'res3-----');
  
//   const res3Count = await getCount(nestedIndex, "bbb2");
//   console.log(res3Count, 'res3Count-----');
  
// question three: 数组索引的查询
// const hobbyindex = objStore1.index("hobbies");
// const res4 = await getData(hobbyindex, "4440", false, 1);
// console.log(res4, 'res4-----');

// const res4Count = await getCount(hobbyindex, "1120");
// console.log(res4Count, 'res3Count-----');
  
// question four: 组合索引的查询
// const compIndex = objStore1.index("name_age");
// const keyRangeValue = IDBKeyRange.bound(["zhangsan0", Number.NEGATIVE_INFINITY], ["zhangsan100", []], false, false);

// const res5 = await getData(compIndex, keyRangeValue);
// console.log(res5, 'res5-----');

// 跟使用name索引查询的结果是一样的
// const nameIndex = objStore1.index("name");
// const keyRangeValue1 = IDBKeyRange.bound("zhangsan0", "zhangsan100", false, false);

// const res5Count = await getCount(nameIndex, keyRangeValue1);
// console.log(res5Count, 'res5Count-----');

// const compIndex1 = objStore1.index("name_age_height");
// const keyRangeValue2 = IDBKeyRange.bound(["zhangsan0", 1, Number.NEGATIVE_INFINITY], ["zhangsan100", 5, []]);
  
// const res6 = await getData(compIndex1, keyRangeValue2);
// console.log(res6, 'res6-----');


// objStore1.clear();
}, 2000);


参考文献

  1. www.w3.org/TR/IndexedD…