JavaScript IndexedDB 完整指南

时间:2022-09-30 17:57:14

本文将通过一个crmeb小教程向你介绍 ​​IndexedDB​​​,并将 ​​IndexedDB​​​ 与其他可用选项进行比较。​​IndexedDB​​ 用于在浏览器中存储数据,对于需要离线工作的 web 应用程序(如大多数进步的 web 应用程序)尤其重要。

首先,让我们介绍一下为什么需要将数据存储在 web 浏览器中。数据在 web 应用程序中无处不在 —— 用户交互创建数据、查找数据、更新数据和删除数据。如果没有存储这些数据的方法,就不可能允许用户交互跨多个 web 应用程序的使用保持状态。你通常会使用 MySQL、Postgres、MongoDB、Neo4j、ArangoDB 等数据库来处理这些存储,但如果你希望应用程序脱机工作呢?

这在不断发展的 web 应用程序中尤为重要,这些应用程序复制了原生应用程序的感觉,但却位于浏览器中。这些渐进的 web 应用程序必须离线工作,因此需要一个存储选项。幸运的是,有几种关于如何在浏览器中存储数据的工具,可以在线和离线访问数据。


1. 浏览器存储方式

关于如何在浏览器中存储数据,Web 标准提供了三个主要 API:

  • ​Cookies​​:此数据存储在浏览器中,​​Cookies​​ 的大小限制为​​4k​​。通常当服务器响应一个请求时,它们可能包含一个​​SET-COOKIE​​ 头,给浏览器一个要存储的键和值。然后,客户端应该在未来的请求头中包含这个​​cookie​​,这将允许服务器识别浏览器会话等。这些​​cookie​​ 通常具有​​HTTP-Only​​ 属性,这意味着不能通过客户端脚本访问​​cookie​​。这使得​​cookie​​ 不是保存脱机数据的好选择。
  • ​LocalStorage/SessionStorage​​:​​LocalStorage / SessionStorage​​ 是浏览器内置的键值存储,其中每个键的大小限制为​​5MB​​。​​LocalStorage​​ 存储数据,直到删除为止,而​​sessionStorage​​ 将在浏览器关闭时清除自己。除此之外,它们的 API 是相同的。可以使用​​window.localStorage.setItem("Key", "Value")​​ 添加键值对。并使用​​window.localStorage.getItem("Key")​​ 检索一个值。注意, LocalStorage API 是同步的,因此使用它会阻塞浏览器中的其他活动,这可能是一个问题。
  • ​IndexedDB​​:一个内置在浏览器中的完整文档数据库,没有存储限制,它允许你异步访问数据,这对于防止复杂操作阻塞呈现和其他活动非常有效。这就是我们将在下面深入讨论的内容。

在这些方式中,​​localStorage​​ 是进行简单操作和存储少量数据的好选择。对于更复杂或常规的操作,​​IndexedDB​​ 可能是更好的选择,特别是在需要异步获取数据的情况下。

IndexedDB API 比 LocalStorage API 更复杂。所以,让我们用 ​​IndexedDB​​ 构建一些东西,让你更好地感受它是如何工作的!


2. 使用案例

创建一个新的 HTML 文件,我们称之为 ​​index.html​​,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IndexedDB Todo List</title>
<style>
body {
text-align: center;
}

h1 {
color: brown;
}
</style>
</head>
<body>
<main>
<h1>IndexedDB Todo-List</h1>
<div id="form">
<input type="text" placeholder="new todo here">
<button>Add Todo</button>
</div>
<div id="todos">
<ul></ul>
</div>
</main>


<script>
// 保存输入的变量
const textInput = document.querySelector("[type='text']")
const button = document.querySelector("button")
// 保存 todos 的数组
const todos = []
// 渲染 todos 的函数
function renderTodos(){
const ul = document.querySelector("#todos ul")
ul.innerHTML = ""
for (todo of todos){
ul.innerHTML += `<li>${todo}</li>`
}
}
renderTodos()
</script>

</body>
</html>
复制代码

现在我们可以开始设置 ​​IndexedDB​​ 了。在浏览器中打开此文件。如果你正在使用 VS Code,可以用像 ​​liveserver​​ 这样的扩展。

​IndexedDB​​ 支持非常好,但我们仍然想检查浏览器是否支持 API 的实现,以便你可以添加以下函数来检查。

// 检查 indexedDB 实现并返回它的函数
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​​ 的浏览器实现,要么返回浏览器不支持的日志。你可以记录在浏览器中调用 ​​getIndexDB​​ 的结果,以确认浏览器支持 ​​IndexedDB​​。

下面你可以看到兼容性列表。你可以在这里找到完整的列表,包括移动浏览器。

JavaScript IndexedDB 完整指南

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

const indexedDB = getIndexDB()
// console.log(indexedDB)
const request = indexedDB.open("todoDB", 1)
console.log(request)
renderTodos();
复制代码

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

const indexedDB = getIndexDB()
// console.log(indexedDB)
const request = indexedDB.open("todoDB", 1)
//console.log(request)
// onerror 处理
request.onerror = (event) => console.error("IndexDB Error: ", event)

renderTodos();
复制代码

我们将监听的下一个事件是 ​​request.onupgradeneeded​​ 事件,当试图打开一个版本号高于数据库当前版本号的数据库时,该事件就会运行。这是创建存储 / 表及其模式的函数。这个函数在每个版本号下只执行一次。因此,如果你决定更改 ​​onupgradedened​​ 回调来更新你的模式或创建新的存储,那么版本号也应该在下一个 ​​.open​​ 调用中增加。存储本质上相当于传统数据库中的表。

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​​ 中,我们做了以下几点:

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

最后要处理 ​​request.onsuccess​​ 事件,该事件在数据库连接和存储全部设置和配置之后运行。你可以利用这个机会提取 ​​todo​​ 列表并将它们注入到我们的数组中。

//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 (todo of query.result){
todos.push(todo.text)
}
renderTodos()
}
}
复制代码

在 ​​onsuccess​​ 中,我们做了以下几点:

  • 获取数据库连接
  • 创建事务
  • 指定我们在哪个存储上进行事务处理
  • 运行一个​​getAll​​ 查询来获取存储中的所有文档 / 记录
  • 在查询特定的​​onsuccess​​ 事件中,我们循环遍历​​todos​​,将它们存入​​todos​​ 数组并调用​​renderTodos()​​,因此它们被渲染到 dom 中

你应该在控制台中看到一个 ​​console.log​​,其中包含一个空数组。

** 错误提示:** 如果你正在运行一个热重新加载 web 服务器,如 liveserver,你可能会看到一个错误,没有存储。这是因为 ​​onupgradedneeded​​ 函数在你写完函数之前就执行了。因此,它不会为该版本号再次执行。解决方案是增加表的版本号,这将创建一个 ​​onupgradenneeded​​,并且 ​​onupgradenneeded​​ 回调将在下次页面刷新时执行。

现在我们已经有了数据库设置,可以对我们希望发生的任何其他事件遵循相同的模式。例如,让我们在单击按钮时创建一个事件,该事件不仅会向 dom 添加一个新的 ​​todo​​,还会向数据库添加一个新的 ​​todo​​,以便在页面刷新时显示。

// button 事件
button.addEventListener("click", (event) => {
// 设置一个事务
const db = request.result
const tx = db.transaction("todos", "readwrite")
const todosStore = tx.objectStore("todos")
// 增加一个 todo
const text = textInput.value
todos.push(text) // 增加一个 todo 到数组
todosStore.put({text}) // 添加到 indexedDB
renderTodos() // 更新 dom
})
复制代码

现在你可以添加 ​​todos​​,因为你使用的是 ​​IndexedDB​​,无论你是在线还是离线,它都可以工作。

添加一些 ​​todo​​,当你刷新页面时,你将看到 ​​todo​​ 持续存在。它们也会显示在查询结果的 ​​console.log​​ 中,每个 ​​todo​​ 都有一个唯一的 ID。到目前为止,完整的代码应该如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IndexedDB Todo List</title>
<style>
body {
text-align: center;
}
h1 {
color: brown;
}
</style>
</head>
<body>
<main>
<h1>IndexedDB Todo-List</h1>
<div id="form">
<input type="text" placeholder="new todo here" />
<button>Add Todo</button>
</div>
<div id="todos">
<ul></ul>
</div>
</main>

<script>
// 保存输入的变量
const textInput = document.querySelector("[type='text']");
const button = document.querySelector("button");
// 保存 todos 的数组
const todos = [];
// 渲染 todos 的函数
function renderTodos() {
const ul = document.querySelector("#todos ul");
ul.innerHTML = "";
for (todo of todos) {
ul.innerHTML += `<li>${todo}</li>`;
}
}
// 检查 indexedDB 实现并返回它的函数
function getIndexDB() {
const indexedDB =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB ||
window.shimIndexedDB;
if (indexedDB) {
return indexedDB;
}
console.log("indexedDB not supported by this browser");
return null;
}
const indexedDB = getIndexDB();
// console.log(indexedDB)
const request = indexedDB.open("todoDB", 2);
// console.log(request)
// onerror 处理
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})
};
// onsuccess
request.onsuccess = () {
console.log("Database Connection Established")
// 获取数据库连接
const db = request.result
// 创建事务对象
const tx = db.transaction("todos", "readwrite")
// 创建一个我们的存储事务
const todosStore = tx.objectStore("todos")
// 获取所有 todo
const query = todosStore.getAll()
// 使用数据查询
query.onsuccess = () {
console.log("All Todos: ", query.result)
for (todo of query.result){
todos.push(todo.text)
}
renderTodos()
}
}
// button 事件
button.addEventListener("click", (event) => {
// 设置一个事务
const db = request.result
const tx = db.transaction("todos", "readwrite")
const todosStore = tx.objectStore("todos")
// 添加一个 todo
const text = textInput.value
todos.push(text) // 添加 todo 到数组
todosStore.put({text}) // 添加到 indexedDB
renderTodos() // 更新 dom
})

renderTodos();
</script>

</body>
</html>
复制代码

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

  • ​clear​​: 删除​​store​​ 中的所有记录
  • ​add​​:用给定的​​id​​ 插入一个记录(如果它已经存在就会出错)
  • ​put​​:用给定的​​id​​ 插入或更新一个记录(如果已经存在就会更新)
  • ​get​​:用特定的​​id​​ 获取记录
  • ​getAll​​:从​​store​​ 中获取所有记录
  • ​count​​:返回​​store​​ 中的记录数
  • ​createIndex​​:基于给定的​​index​​ 创建对象来查询
  • ​delete​​: 对给定​​id​​ 进行删除记录


3. 性能和其他考虑因素

你需要考虑以下几点:

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

你可以在 ​​MDN 文档​​中找到更多 ​​IndexedDB​​ 的限制。

虽然 ​​indexedDB​​ 非常适合让你的应用程序离线工作,但它不应该成为你的主数据存储。在互联网连接中,你可能希望将 ​​indexedDB​​ 与外部数据库同步,以便在用户清除浏览器数据时不会丢失用户的信息。