IndexedDB指南、教程

IndexedDB 用于在浏览器中存储数据,对于需要离线工作的 web 应用程序(如大多数渐进式 web 应用程序)特别重要。

首先,让我们了解一下为什么你可能需要在web浏览器中存储数据。在 web 应用程序中,数据无处不在——用户交互会创建数据、查找数据、更新数据和删除数据。如果没有办法存储这些数据,你将无法让用户交互在多次使用 web 应用程序时保持状态。你通常会使用 MySQLPostgresMongoDBNeo4jArangoDB 等数据库来处理这些存储,但如果你希望应用程序离线工作呢?

这在渐进式 web 应用程序(PWA)日益流行的趋势中尤其重要的,这类应用程序复制了原生应用的感觉,但运行在浏览器中。这些渐进式 web 应用程序必须能够离线工作,因此需要一种存储选项。幸运的是,有几种工具可以在浏览器中存储数据,使其可以在线和离线访问。

浏览器存储选项

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:这是一个内置于浏览器中的完整文档数据库,没有存储限制,允许你异步访问数据,因此非常适合防止复杂操作阻塞渲染和其他活动。

在这些选项的背景下,LocalStorage 适用于简单操作和存储少量数据。对于更复杂或常规的操作,IndexedDB 可能是更好的选择,特别是当你需要异步获取数据时。

`IndexedDB API 比 LocalStorage API 更复杂。因此,让我们构建一个使用 IndexedDB 的示例,以便你更好地了解它是如何工作的!

开始学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IndexedDB Todo-List</title>
</head>
<body>
<h1>IndexedDB Todo-List</h1>
<form id="todo-form">
<input type="text" id="new-todo" placeholder="new todo here">
<button type="submit">Add Todo</button>
</form>
</body>
</html>

这设置了一个基本的Todolist外壳。现在我们可以开始设置 ·IndexedDB·。将此文件在浏览器中打开。

连接到 IndexedDB

IndexedDB 的支持相当不错,但我们首先需要检查浏览器是否支持该 API 的实现,因此你可以添加以下函数进行检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
function getIndexDB() {
const indexedDB =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB ||
window.shimIndexedDB;
if (indexedDB) {
return indexedDB;
}
console.error("indexedDB not supported by this browser");
return null;
}

这个函数将返回浏览器对 IndexedDB 的实现,或者记录浏览器不支持 IndexedDB。你可以在浏览器中调用 getIndexDB 并记录结果,以确认你的浏览器是否支持 IndexedDB。以下是来自 caniuse 的桌面浏览器兼容性列表,你可以在这里找到包括移动浏览器在内的完整列表。

打开数据库

现在让我们使用 indexedDB.open("数据库名", 1) 打开一个数据库。open 方法的第一个参数是数据库的名称,第二个参数是数据库的版本号。如果你希望触发 onupgradeneeded 事件,你应该增加版本号。open 方法将返回一个对象,该对象具有几个属性,包括 onerroronupgradeneededonsuccess,每个属性都接受一个回调函数,当相关事件发生时执行。

1
2
3
4
5
const indexedDB = getIndexDB();
// console.log(indexedDB)
const request = indexedDB.open("todoDB", 1);
console.log(request);
renderTodos();

你应该会在控制台中看到一个记录的IDBOpenDBRequest对象。IndexedDB 是基于事件的,这符合其异步模型。接下来,让我们监听数据库启动时可能发生的事件。首先,我们监听 request.onerror 事件,以防访问数据库时出现任何错误。

1
2
3
4
5
6
7
8
const indexedDB = getIndexDB();
// console.log(indexedDB)
const request = indexedDB.open("todoDB", 1);
//console.log(request)
//onerror handling
request.onerror = (event) => console.error("IndexDB Error: ", event);

renderTodos();

下一个事件是 request.onupgradeneeded,该事件在尝试以高于当前版本号的版本号打开数据库时运行。这是创建存储(stores)/表及其架构的地方。这个函数每个版本号只执行一次。因此,如果你决定更改 onupgradeneeded 回调以更新架构或创建新存储,则应在下一次 open 调用中增加版本号。存储相当于传统数据库中的表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const indexedDB = getIndexDB();
// console.log(indexedDB)
const request = indexedDB.open("todoDB", 1);
//console.log(request)
//onerror handling
request.onerror = (event) => console.error("IndexDB Error: ", event);
//onupgradeneeded
request.onupgradeneeded = () => {
// 获取数据库连接
const db = request.result;
// 定义新的存储
const store = db.createObjectStore("todos", {
keyPath: "id",
autoIncrement: true,
});
// 指定属性为索引
store.createIndex("todos_text", ["text"], { unique: false });
};

renderTodos();

onupgradeneeded 回调中,我们执行以下操作:

    1. 获取数据库对象(如果 onupgradeneeded 函数正在运行,你知道它是可用的)。
    1. 创建一个名为 todos 的新存储/表/集合,其键为 id,这是一个自动递增的数字(记录的唯一标识符)。
    1. 指定 todos_text 作为索引,这允许我们以后通过 todos_text 搜索数据库。如果你不计划通过特定属性搜索,则不必创建索引。

最后,我们处理 request.onsuccess 事件,该事件在数据库连接和存储全部设置和配置完毕后运行。你会希望利用这个机会提取待办事项列表并将其注入到我们的数组中。(是的,目前还没有待办事项)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//onsuccess
request.onsuccess = () => {
console.log("Database Connection Established");
// 获取数据库连接
const db = request.result;
// 创建一个事务对象
const tx = db.transaction("todos", "readwrite");
// 创建一个事务与我们的存储
const todosStore = tx.objectStore("todos");
// 获取所有待办事项
const query = todosStore.getAll();
// 使用查询中的数据
query.onsuccess = () => {
console.log("All Todos: ", query.result);
for (const todo of query.result) {
todos.push(todo.text);
}
renderTodos();
};
};

onsuccess 中,我们执行以下操作:

    1. 获取数据库连接。
    1. 创建一个事务。
    1. 指定我们正在处理的存储。
    1. 运行 getAll 查询以获取该存储中的所有文档/记录。
    1. 在查询特定的 onsuccess 事件中,我们循环遍历所有待办事项,将它们推送到 todos 数组中,并调用 renderTodos() 以便将它们渲染到 DOM 中。

你应该会在控制台中看到一个空数组的 console.log

现在我们已经设置了数据库,可以按照相同的模式处理任何其他事件。例如,让我们创建一个事件,当你点击按钮时,不仅将一个新的待办事项添加到 DOM 中,还将其添加到数据库中,以便它在页面刷新时显示。

1
2
3
4
5
6
7
8
9
10
11
12
// button event
button.addEventListener("click", (event) => {
// 设置事务
const db = request.result;
const tx = db.transaction("todos", "readwrite");
const todosStore = tx.objectStore("todos");
// 添加待办事项
const text = textInput.value;
todos.push(text); // 添加到待办事项数组
todosStore.put({ text }); // 添加到 IndexedDB
renderTodos(); // 更新 DOM
});

现在你可以添加待办事项,并且由于你正在使用 IndexedDB,无论你是在线还是离线,它都可以工作。

添加待办事项并使用 IndexedDB

添加一些待办事项并刷新页面,你会看到待办事项持续存在。它们还会显示在查询结果的 console.log 中,每个待办事项都有一个唯一的 ID。到目前为止的完整代码如下所示:

todosStore 对象上的其他方法,可用于不同类型的事务:

  • clear - 删除存储中的所有文档/记录
  • add - 插入具有给定 ID 的记录(如果记录已存在则会报错)
  • put - 插入或更新具有给定 ID 的记录(如果记录已存在则会更新)
  • get - 获取具有特定 ID 的记录
  • getAll - 获取存储中的所有记录/文档
  • count - 返回存储中的记录数量
  • createIndex - 创建对象以基于声明的索引进行查询
  • delete - 删除具有给定 ID 的文档

性能和其他考虑事项

需要考虑的几个问题:

  • 在所有浏览器中存储文件作为 Blob 可能不受支持,你会发现将它们存储为 ArrayBuffers 支持更好。
  • 一些浏览器可能不支持在隐私浏览模式下写入 IndexedDB
  • IndexedDB 在写入对象时会创建结构化克隆,这会阻塞主线程,因此如果你有大量嵌套对象,这可能会导致一些延迟。
  • 如果用户关闭浏览器,任何未完成的事务都有可能被中止。
  • 如果另一个浏览器标签页打开了带有较新数据库版本号的应用程序,它将被阻止升级,直到所有旧版本的标签页都关闭/重新加载。幸运的是,你可以使用 onblocked 事件来触发警报,通知用户需要这样做。

尽管 IndexedDB 对于使你的应用程序离线工作非常有用,但它不应该是你的主要数据存储。一旦有互联网连接,你可能需要将 IndexedDB 与外部数据库同步,以防用户清除浏览器数据时丢失信息。

结论

IndexedDB 为你提供了一个强大的异步文档数据库在浏览器中。IndexedDB API 可能有些繁琐,但有一些库如 Dexie 可以为 IndexedDB 提供更易于使用的封装。