IndexedDB

150 阅读10分钟

1. 概述

1.1 浏览器存储选项

渐进式 web应用程序(PWA)运行在浏览器中,必须能够离线工作,因此需要一种存储选项。目前有几种工具可以在浏览器中存储数据,使其可以在线和离线访问。

Web标准为你提供了三种主要的API用于在浏览器中存储数据:

  1. Cookies:这些数据存储在浏览器中,Cookies的大小限制为4KB。通常,当服务器响应请求时,他们可能会包含一个SET-COOKIE 头,给浏览器一个要存储的键和值。客户端应该在未来的请求头中包含此 Cookie,这将允许服务器识别浏览器会话等。这些Cookies通常具有 HTTP-Only 属性,这意味着客户端脚本无法访问 Cookie。因此,Cookies不适合用于存储离线数据。

  2. LocalStorage/SessionStorageLocalStorage/SessionStorage是内置于浏览器中的键值存储,每个键的大小限制为5MBLocalStorage 会存储数据直到被删除,而 SessionStorage 会在浏览器关闭时清除数据。除此之外,它们的 API 是相同的。你可以使用 window.localStorage.setItem("Key", "Value") 添加键值对,并使用 window.localStorage.getItem("Key") 检索值。注意,LocalStorage API是同步的,因此使用它会阻塞浏览器中的其他活动。

  3. IndexedDB:这是一个内置于浏览器中的完整文档数据库,没有存储限制,允许异步访问数据,因此非常适合防止复杂操作阻塞渲染和其他活动。就数据库类型而言,IndexedDB不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。

1.2 IndexedDB使用场景

所有的场景都基于客户端需要存储大量数据的前提下:

  1. 数据可视化等界面,大量数据,每次请求会消耗很大性能。

  2. 即时聊天工具,大量消息需要存在本地。

  3. 其它存储方式容量不满足时,不得已使用IndexedDB

1.3 IndexedDB的优势

  • IndexedDB提供了类似数据库风格的数据储存和使用方式.

  • Cookie只能是字符串,储存空间有限,每次HTTP接收和发送都会传递Cookies数据,它会占用额外的流量.

  • LocalStorage是用key-value 键值模式储存数据,想让localstorage存储对象,你需要借助JSON.stringify()能将对象变成字符串形式,再用JSON.parse()将字符串还原成对象,当存储的数据庞大时,这就不是最佳的方案了,LocalStorage就是专门为小数量数据设计的,它的api设计为同步的.

  • IndexedDB很适合存储大量数据,它的API是异步调用的,IndexedDB使用索引存储数据,各种数据库操作放在事务中执行,IndexedDB支持简单的数据类型,它比LocalStorage强大,API也相对复杂,对于简单的数据,还是使用LocalStorage

  • IndexedDB能提供更为复杂的查询数据的方式。

1.4 IndexedDB的特性

  1. 键值对储存

    • IndexedDB没有表的概念,内部采用对象仓库objectstore存放数据。

    • 一个数据库中可以包含多个objectStore

    • objectStore是一个灵活的数据结构,可以存放多种类型数据,也就是说一个objectStore相当于一张表,里面储存的每条数据和一个键相关。

    • 我们可以使用每条记录中的某个字段作为键值keyPath,也可以使用自动生成的递增数字作为键值keyGenerator,也可以不指定。

    • 选择键的类型不同,objectStore可以存储的数据结构也有差异

  2. 事务性

    • IndexedDB中,每一个对数据库操作是在一个事务的上下文中执行的。这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

    • 事务范围一次影响一个或多个objectstores

    • 你通过传入一个objectstores名字的数组到创建事务范围的函数来定义。

      • 比如: db.transaction( storeName, ' readwrite' ),创建事务的第二个参数是事务模式,当请求一个事务时,必须决定是按照只读还是读写模式模式请求访问
  3. 基于请求

    • IndexedDB数据库的每次操作,描述为通过一个请求打开数据库,访问一个objectstore,再继续。

    • IndexedDB API天生是基于请求的,这也是API异步本性指示,对于你在数据库执行的每次操作,你必须首先为这个操作创建一个请求,当请求完成,你可以响应由请求结果产生的事件和错误

  4. 异步

    • IndexedDB大部分操作并不是我们常用的调用方法,返回结果的模式,而是请求 ----> 响应的模式
    • 所谓异步API是指并不是这条指令执行完毕,我们就可以使用request.result来获取IndexedDB对象了。
    • 类似于 我们使用ajax一样,语句执行完并不代表已经获取到了对象,所以我们一般在其回调函数中处理。

2. IndexedDB重要概念

IndexedDB是一个比较复杂的 API,它把不同的实体,抽象成一个个对象接口:

  • 数据库:IDBDatabase对象
  • 对象仓库:IDBObjectStore对象
  • 索引: IDBIndex对象
  • 事务: IDBTransaction对象
  • 操作请求:IDBRequest对象
  • 指针: IDBCursor对象
  • 主键集合:IDBKeyRange对象

下面是一些主要的概念。

  1. 数据库

数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。

IndexedDB数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。

  1. 对象仓库

每个数据库包含若干个对象仓库objectstore。它类似于关系型数据库的表格。

  1. 数据记录

对象仓库保存的是数据记录。每条记录类似于关系型数据库的行,但是只有主键和数据体两部分。主键用来建立默认的索引,必须是不同的,否则会报错。主键可以是数据记录里面的一个属性,也可以指定为一个递增的整数编号。

{ id: 1, text: 'foo' }

上面的对象中,id属性可以当作主键。

数据体可以是任意数据类型,不限于对象。

  1. 索引

为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。

  1. 事务

数据记录的读写和删改,都要通过事务完成。事务对象提供errorabortcomplete三个事件,用来监听操作结果。

3. IndexedDB流程

IndexedDB 数据库的各种操作,一般是按照下面的流程进行的

3.1 打开数据库

使用 IndexedDB 的第一步是打开数据库,使用indexedDB.open()方法。

var request = window.indexedDB.open(databaseName, version);

这个方法接受两个参数,第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本。如果省略,打开已有数据库时,默认为当前版本;新建数据库时,默认为1

indexedDB.open()方法返回一个 IDBRequest 对象。这个对象通过三种事件errorsuccessupgradeneeded,处理打开数据库的操作结果。

1. error 事件

error事件表示打开数据库失败。

request.onerror = function (event) {
  console.log('数据库打开报错');
};

2. success 事件

success事件表示成功打开数据库。

var db;

request.onsuccess = function (event) {
  db = request.result;
  console.log('数据库打开成功');
};

这时,通过request对象的result属性拿到数据库对象。

3. upgradeneeded 事件

如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件upgradeneeded

var db;

request.onupgradeneeded = function (event) {
  db = event.target.result;
}

这时通过事件对象的target.result属性,拿到数据库实例。

3.2 新建数据库

新建数据库与打开数据库是同一个操作。如果指定的数据库不存在,就会新建。不同之处在于,后续的操作主要在upgradeneeded事件的监听函数里面完成,因为这时版本从无到有,所以会触发这个事件。

通常,新建数据库以后,第一件事是新建对象仓库(即新建表)。

request.onupgradeneeded = function(event) {
  db = event.target.result;
  var objectStore = db.createObjectStore('person', { keyPath: 'id' });
}

上面代码中,数据库新建成功以后,新增一张叫做person的表格,主键是id

更好的写法是先判断一下,这张表格是否存在,如果不存在再新建。

request.onupgradeneeded = function (event) {
  db = event.target.result;
  var objectStore;
  if (!db.objectStoreNames.contains('person')) {
    objectStore = db.createObjectStore('person', { keyPath: 'id' });
  }
}

主键(key)是默认建立索引的属性。比如,数据记录是{ id: 1, name: '张三' },那么id属性可以作为主键。主键也可以指定为下一层对象的属性,比如{ foo: { bar: 'baz' } }foo.bar也可以指定为主键。

如果数据记录里面没有合适作为主键的属性,那么可以让 IndexedDB 自动生成主键。

var objectStore = db.createObjectStore(
  'person',
  { autoIncrement: true }
);

上面代码中,指定主键为一个递增的整数。

新建对象仓库以后,下一步可以新建索引。

request.onupgradeneeded = function(event) {
  db = event.target.result;
  var objectStore = db.createObjectStore('person', { keyPath: 'id' });
  objectStore.createIndex('name', 'name', { unique: false });
  objectStore.createIndex('email', 'email', { unique: true });
}

上面代码中,IDBObject.createIndex()的三个参数分别为索引名称、索引所在的属性、配置对象(说明该属性是否包含重复的值)。

3.3 新增数据

新增数据指的是向对象仓库写入数据记录。这需要通过事务完成。

function add() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .add({ id: 1, name: '张三', age: 24, email: 'zhangsan@example.com' });

  request.onsuccess = function (event) {
    console.log('数据写入成功');
  };

  request.onerror = function (event) {
    console.log('数据写入失败');
  }
}

add();

上面代码中,写入数据需要新建一个事务。新建时必须指定表格名称和操作模式("只读"或"读写")。新建事务以后,通过IDBTransaction.objectStore(name)方法,拿到 IDBObjectStore 对象,再通过表格对象的add()方法,向表格写入一条记录。

写入操作是一个异步操作,通过监听连接对象的success事件和error事件,了解是否写入成功。

3.4 读取数据

读取数据也是通过事务完成。

function read() {
   var transaction = db.transaction(['person']);
   var objectStore = transaction.objectStore('person');
   var request = objectStore.get(1);

   request.onerror = function(event) {
     console.log('事务失败');
   };

   request.onsuccess = function( event) {
      if (request.result) {
        console.log('Name: ' + request.result.name);
        console.log('Age: ' + request.result.age);
        console.log('Email: ' + request.result.email);
      } else {
        console.log('未获得数据记录');
      }
   };
}

read();

上面代码中,objectStore.get()方法用于读取数据,参数是主键的值。

3.5 遍历数据

遍历数据表格的所有记录,要使用指针对象 IDBCursor。

function readAll() {
  var objectStore = db.transaction('person').objectStore('person');

   objectStore.openCursor().onsuccess = function (event) {
     var cursor = event.target.result;
     
     if (cursor) {
       console.log('Id: ' + cursor.key);
       console.log('Name: ' + cursor.value.name);
       console.log('Age: ' + cursor.value.age);
       console.log('Email: ' + cursor.value.email);
       cursor.continue();
    } else {
      console.log('没有更多数据了!');
    }
  };
}

readAll();

上面代码中,新建指针对象的openCursor()方法是一个异步操作,所以要监听success事件。

3.6 更新数据

更新数据要使用IDBObject.put()方法。

function update() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .put({ id: 1, name: '李四', age: 35, email: 'lisi@example.com' });

  request.onsuccess = function (event) {
    console.log('数据更新成功');
  };

  request.onerror = function (event) {
    console.log('数据更新失败');
  }
}

update();

上面代码中,put()方法自动更新了主键为1的记录。

3.7 删除数据

IDBObjectStore.delete()方法用于删除记录。

function remove() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .delete(1);

  request.onsuccess = function (event) {
    console.log('数据删除成功');
  };
}

remove();

3.8 使用索引

索引的意义在于,可以让你搜索任意字段,也就是说从任意字段拿到数据记录。如果不建立索引,默认只能搜索主键(即从主键取值)。

假定新建表格的时候,对name字段建立了索引。

objectStore.createIndex('name', 'name', { unique: false });

现在,就可以从name找到对应的数据记录了。

var transaction = db.transaction(['person'], 'readonly');
var store = transaction.objectStore('person');
var index = store.index('name');
var request = index.get('李四');

request.onsuccess = function (e) {
  var result = e.target.result;
  if (result) {
    // ...
  } else {
    // ...
  }
}