IndexedDB 中文教程(2)基本使用

IndexedDB 中文教程(1)介绍

IndexedDB 中文教程(2)基本使用


教程持续更新中……

本文将基于一些代码实例对于 IndexedDB 的基本用法进行介,为了教程的连贯性,一些原理性的内容和容易有疑问的地方并未展开,在文末将会对这些疑问进行解释和说明。

连接数据库

使用 indexedDB.open() 方法打开本地数据库,如果该数据库不存在,则创建该数据库。该方法接受两个参数:数据库名、数据库版本(可选的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var openRequest = window.indexedDB.open('hozen.site');
var db;
openRequest.onsuccess = function(event) {
db = this.result;
// db = event.target.result;
// db = openRequest.result;
console.log("打开数据库成功");
}
openRequest.onupgradeneeded = function(event) {
var objStore = this.result.createObjectStore('blog', {keyPath: 'url'});
objStore.createIndex('category', 'category'); // 创建索引
console.log('创建对象仓库完成');
}
openRequest.onerror = function(event) {
console.log('打开数据库失败', event);
}

open 方法是异步的,当打开成功时触发 onsuccess 事件,失败时触发 onerror 事件。当数据库升级时,触发 onupgradeneeded 事件。升级是指该数据库首次被创建,或调用 open() 方法时指定的数据库的版本号高于本地已有的版本。

onupgradeneeded 处理函数对于创建数据库和修改数据库结构是很重要的,因为 IndexedDB 只允许在该函数内创建和删除 ObjectStore 或创建删除索引。一旦错过这个机会就无法在当前版本数据库中对数据库结构进行修改了。

db 变量保存了打开数据库请求的结果,当打开数据库成功时将获取一个对象。该对象是对 IndexedDB 进行数据操作的基础,因此要及时保存它,并保证在需要的地方能调用得到。

createObjectStore 方法创建了一个对象仓库,该方法接受两个参数:仓库名、配置对象(可选的)。该例中,使用配置对象指定了 'url' 为对象仓库的内键,其作用类似关系型数据库中的主键,需具有唯一标识性。该方法返回新创建的对象仓库。

添加一条数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var data = {
title: 'IndexedDB 教程(1)简介',
url: 'https://www.hozen.site/archives/6/',
categories: 'clientScript',
};
var transaction = db.transaction('blog', 'readwrite');
var objectStore = transaction.objectStore('blog');
var addReq = objectStore.add(data);
addReq.onsuccess = function() {
console.log('添加数据成功', this.result);
}
addReq.onerror = function(event) {
console.log('添加数据失败', event)
}

这里我们直接将一个 JavaScript 对象 data 未加任何处理的存入了数据库。在该例中由于我们使用了内键,因此我们只能存储 JavaScript 对象(或者是因为我们只会存储对象,所以才使用了内键),并且其 url 属性是必须且唯一的。

首先,通过 transaction() 方法获取一个事务,该方法接受两个参数。第一个参数作为事务的作用域,表示该事务跨越的对象仓库;第二个参数表示事务权限,这里需要一个读写权限。该方法返回一个事务对象。

objectStore() 方法获取指定对象仓库,该方法一次只能获取一个对象仓库。

使用对象仓库的 add() 方法像数据库添加数据,这同样是一个异步方法,触发响应事件处理函数。

取出一条数据

1
2
3
4
5
6
7
8
9
var transaction = db.transaction('blog');
var objectStore = transaction.objectStore('blog');
var getReq = objectStore.get('https://www.hozen.site/archives/6/');
getReq.onsuccess = function() {
console.log('读取数据成功: ', this.result);
}
getReq.oerror = function(event) {
console.log('读取数据失败', event);
}

类似的,我们启动一个事务,然后获取一个对象仓库,调用它的 get() 方法读取数据,然后对成功和失败的情况做处理。但这里稍有不同的是,我们只需要一个“只读”事务即可(最小权限原则不仅是优秀的代码习惯,在 IndexedDB 对于跨作用域的情况也是有积极作用的,见文末)。

get() 方法接受数据的键(key)作为参数,从数据库中查询对应的数据。如果查询成功,其结果会返回至请求对象的 result 属性中。数据为空返回 undefined

修改一条数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var dataNew = {
title: 'IndexedDB 教程(1)简介',
url: 'https://www.hozen.site/archives/6/',
categories: 'clientScript',
alias: 'indexDB introduction',
};
var transaction = db.transaction('blog', 'readwrite');
var objectStore = transaction.objectStore('blog');
var addReq = objectStore.put(dataNew);
addReq.onsuccess = function() {
console.log('修改数据成功', this.result);
}
addReq.onerror = function(event) {
console.log('修改数据失败', event)
}

这里的代码基本上和添加数据的代码一致,只不过我们向数据中新增了一个 alias 字段。然后调用了 put() 方法而非 add() 方法向数据库插入数据。如果这里我们再次使用 add() 方法将修改后的数据插入数据库时,请求会失败,因为已经存在一个 url 相同的数据。而 put() 则是不关心数据库是否已存在该数据(具备相同键的数据),总是把数据插入数据库,进而覆盖掉以前的数据,达到更新数据的目的。

因此,在 IndexedDB 中更新数据实际上相当于用一个新的数据覆盖掉原来的数据,所以当我们只更新部分数据时,会先从数据库中取出原来的数据,然后修改该数据的属性,最后将该数据“put”进数据库。这和关系型数据库中 UPDATE 语法不太一样。

删除一条数据

1
2
3
4
5
6
7
8
9
var transaction = db.transaction('blog', 'readwrit');
var objectStore = transaction.objectStore('blog');
var delReq = objectStore.delete('https://www.hozen.site/archives/6/');
delReq.onsuccess = function() {
console.log('删除数据成功');
}
delReq.onerror = function(event) {
console.log('删除数据失败', event);
}

同样我们需要启动一个事务来进行删除操作,delete 方法接受一个参数来删除对应的数据,该参数可以是一条数据的键,也可以是一个代表键范围的对象 IDBKeyRange。删除成功时触发 onsuccess 事件,删除一个不存在的数据时,同样也会成功。

批量操作

批量插入

使用循环来批量插入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var transaction = db.transaction('blog', 'readwrite');
var objectStore = transaction.objectStore('blog');
var dataSet = [
{
title: 'IndexedDB 中文教程(1)简介',
url: 'https://www.hozen.site/archives/6/',
category: 'clientScript',
}, {
title: 'IndexedDB 中文教程(2)基本用法',
url: 'https://www.hozen.site/archives/7/',
category: 'clientScript',
}, {
title: '《和解》—— React 差异化算法',
url: 'https://www.hozen.site/archives/1/',
catefory: 'React',
}, {
title: '[译]严格判定JavaScript对象是否为数组',
url: 'https://www.hozen.site/archives/2/',
category: 'ECMAScript',
}, {
title: 'JavaScript设计模式(1)——单例模式',
url: 'https://www.hozen.site/archives/17/',
category: 'ECMAScript',
}];

transaction.oncomplete = function() {
console.log('所有数据插入成功');
};

transaction.onerror = function(event) {
console.log('插入数据失败:', event);
}

dataSet.forEach(function(data) {
objectStore.put(data);
});

上例中使用了事务的 oncomplete 事件,当事务完成时会触发该事件。

查询所有数据

使用 getAll() 可以进行查询对象仓库所有数据:

1
2
3
4
5
6
7
8
var objectStore = db.transaction('blog').objectStore('blog');
var getAllReq = objectStore.getAll();
getAllReq.onsuccess = function() {
console.log('所有数据', this.result);
};
getAllReq.onerror = function(evnt) {
console.log('获取数据失败', event);
};

该方法有两个可选参数,第一个参数指定查询范围 IDBKeyRange,第二个参数限制返回数据的数量。

相应的 getAllKeys() 可以获取所有数据的键。

清空对象仓库

在删除数据小节里已经提到,传入一个 IDBKeyRange 对象参数给 delete() 方法可以实现范围删除。如果需要清空对象仓库,有一个更简单的方法:clear()

1
2
3
4
5
6
7
8
var objectStore = db.transaction('blog', 'readwrite').objectStore('blog');
var clearReq = objectStore.clear();
clearReq.onsuccess = function() {
console.log('清空成功');
}
clearReq.onerror = function(event) {
console.log('清空失败', event);
}

使用游标

使用游标可以对对象仓库中的数据进行遍历:

1
2
3
4
5
6
7
8
9
10
var objectStore = db.transaction('blog').objectStore('blog');
cursorReq.onsuccess = function() {
var cursor = this.result;
if (cursor) {
console.log(cursor.value.title);
cursor.continue();
} else {
console.log('遍历结束');
}
}

openCursor() 方法有两个可选参数,第一个仍然是一个 IDBKeyRange 对象来指定遍历范围,第二个参数用来指定遍历方向。当查询成功时,cursorReqresult 属性指向一个游标对象,该对象的 keyvalue 分别保存了查询数据的键值。调用 cursorcontinue() 方法使遍历继续吗,当遍历结束时 result 得到 null

使用 getAll() 获取所有所有数据,然后对结果进行遍历获取 title 属性,也可以达到上例的目的。但如果我们只想获取 key 的值时,使用游标要比使用 getAll() 高效得多。

相应的 openKeyCursor 可以用来获取数据的键。

使用索引

本文中的例子使用了 url 作为对象仓库的内键以保证不会重复,但不会重复的键大多不易记忆和查询。IndexedDB 提供了索引机制。

1
2
3
4
5
6
7
8
9
var objectStore = db.transaction('blog').objectStore('blog');
var index = objectStore.index('category');
var indexReq = index.getAll();
indexReq.onsuccess = function() {
console.log(this.result);
}
indexReq.onerror = function(event) {
console.log(event);
}

可能存在的疑问

以上是一些基本的 IndexedDB 的 API,尽量避免了一些可能稍微复杂的概念和原理,但如果想解决实践过程中遇到的一些问题,这些原理必须要掌握。

比如,新学者在使用 IndexedDB 的过程中可能会遇到以下问题:

  • 事务终止,无法调用
  • 请求一直处于 pending 状态,无法进行数据的版本升级
  • 启用不必要的事务,浪费性能
  • 事务跨作用域失败
  • 选择不合适的对象仓库类型

这些问题的排查和解决需要对 IndexedDB 的请求机制,事务生命周期等内容有全面的了解。