# 什么是 indexeddb

indexeddb 是浏览器提供的一种本地存储机制,可以存储大量的支持结构化克隆算法的数据。

# 如何使用 indexeddb

# 打开 indexeddb

1
2
3
const request = indexedDB.open('todo_database');
// with version
const request = indexedDB.open('todo_database', 2);

# 创建 / 删除 indexeddb objectStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// onupgradeneeded 事件会在version变化的时候触发 (创建database也可以理解一种version change, 0 -> 大于0)
request.onupgradeneeded = () => {
const db = request.result;
// 创建objectStore
const ob = db.createObjectStore('todo_store');
// 创建索引
ob.createIndex('id', 'id', { unique: true });

objectStore.transaction.oncomplete = () => {
// 你可以操作 todo_store objectStore 了
// ...

// 删除objectStore
db.deleteObjectStore('todo_store');
};
};

// 只有在 onupgradeneeded 中才可以 创建 / 删除 objectStore
// 所以,如果你需要创建 / 删除 objectStore,需要监听 onupgradeneeded 事件,同时你要在调用 open 的时候设置一个 version 数值比当前的 version 大。

# 处理 indexeddb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
request.onsuccess = () => {
const db = request.result;
// 获取数据库中的数据

const transaction = db.transaction('todo_store');
const store = transaction.objectStore('todo_store');
const request = store.get(1);
request.onerror = () => {
console.log('error');
};
request.onsuccess = () => {
const data = request.result;
console.log(data);
};
};

indexeddb 处理请求都是通过事件来处理的,所以需要监听事件(onsuccess, onerror)。
db.transaction () 会返回一个事务对象,事务对象可以处理多个 store。同时可以配置事务的 mode(readonly, readwrite)。

# 关闭 indexeddb

1
2
// db type is IDBDatabase
db.close();

# IDBDatabase 事件

  1. close
    close 事件会在数据库关闭的时候触发 (注意如果是调用的 db.close (), 那么这个事件不会触发)

  2. versionchange
    versionchange 事件会在数据库的 version 变化的时候触发

# versionchange 有什么妙用吗?

背景:对于多个连接到同一个 database 的实例,如果某个触发 onupgradeneeded 事件,那么其他实例需要断开连接,否则该 onupgradeneeded 事件会 blocked。

  • 考虑这样的一个场景:你需要更新 database 的 schema,但是呢,你又有很多个关于该 database 的实例,你需要一个一个的去调用 db.close () 吗?
  • 答案是:不需要,你可以监听 versionchange 事件,当 versionchange 事件触发的时候,你再调用 db.close ()。
1
2
3
4
db.onversionchange = () => {
db.close();
// 如果你需要重新连接到database,你可以在未来某个时间节点重新调用open方法进行reconnect
}

# IDBCursor

IDBCursor 是 indexeddb 提供的一种游标,可以遍历数据库中的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const request = this.db
.transaction('todo_store', 'readonly')
.objectStore('todo_store')
.openCursor();
const keys: string[] = [];
request.onsuccess = e => {
const cursor = (e.target as any).result as IDBCursorWithValue;
if (!cursor) {
// 没有更多数据了
return;
}
const value = cursor.value
// do something with value
// ...

cursor.continue();
};

# IDBKeyRange

IDBKeyRange 是 indexeddb 提供的一种范围,可以用于查询数据库中的数据。

1
2
3
4
const lower = 'your lower bound';
const upper = 'your upper bound';
const range = IDBKeyRange.bound(lower, upper, false, true);
range 包含范围key: `[lower, upper)`

# IDBTransaction

IDBTransaction 是 indexeddb 提供的一种事务,所有的读取和写入数据均在事务中完成。

1
2
const transaction = db.transaction('todo_store');
const store = transaction.objectStore('todo_store');

提起这个主要是踩了一个坑
最小 example:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IndexedDB Demo</title>
</head>
<body>

<button id="loadButton">Load Data</button>
<div id="output"></div>

<script>
// 1. 打开数据库
const request = indexedDB.open("myDatabase", 1);

let objectStore;

// 2. 数据库创建或更新时的回调
request.onupgradeneeded = function(event) {
const db = event.target.result;
if (!db.objectStoreNames.contains("myStore")) {
const store = db.createObjectStore("myStore", { keyPath: "id" });
store.transaction.oncomplete = function() {
// 初始化数据
const transaction = db.transaction("myStore", "readwrite");
const objectStore = transaction.objectStore("myStore");
objectStore.add({ id: 1, name: "Alice", age: 25 });
objectStore.add({ id: 2, name: "Bob", age: 30 });
};
}
};

// 3. 打开数据库成功时的回调
request.onsuccess = function(event) {
const db = event.target.result;

// 获取 objectStore 并赋值给外部变量
const transaction = db.transaction("myStore", "readwrite");
objectStore = transaction.objectStore("myStore");

console.log("Database opened successfully");

// 绑定 button 点击事件
const loadButton = document.getElementById("loadButton");
loadButton.addEventListener("click", function() {
// 在按钮点击时使用 objectStore 的 get 方法
const getRequest = objectStore.get(1); // 假设我们查询 id 为 1 的数据

getRequest.onsuccess = function(event) {
const result = event.target.result;
document.getElementById("output").textContent = `Name: ${result.name}, Age: ${result.age}`;
};

getRequest.onerror = function(event) {
console.error("Error retrieving data: ", event.target.error);
};
});
};

// 4. 打开数据库失败时的回调
request.onerror = function(event) {
console.error("Database open failed: ", event.target.error);
};
</script>

</body>
</html>

报错:

1
2
Uncaught TransactionInactiveError: Failed to execute 'get' on 'IDBObjectStore': The transaction has finished.
at HTMLButtonElement.<anonymous> (indexedDB-transaction-issue.html:48:48)

原因:

  • 所有请求结束并且控制权返回事件循环之前没有发出其他请求,transaction 会自动提交(finished)。换句话,你可以在上一个请求的成功 / 失败回调中发出请求,但不能在其他异步中处理该事务。对于 finished transaction,任何请求都会报错。
  • 在这个 demo 中,transaction 在 onupgradeneeded 中创建,button click callback 通过闭包获取到了 objectStore。但此时我们的 transaction 已经 finished 了,所以报错
  • 解决方案:在 callback 重新创建一个 transaction