JavaScript IndexedDB 完整指南
本文将通过一个小教程向你介绍 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 是同步的,因此使用它会阻塞浏览器中的其他活动,这可能是一个问题。你可以阅读 JavaScript LocalStorage 完整指南 了解更多关于 LocalStorage 的信息。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
。
下面你可以看到兼容性列表。你可以在这里找到完整的列表,包括移动浏览器。
现在让我们用 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
与外部数据库同步,以便在用户清除浏览器数据时不会丢失用户的信息。
4. 小结
IndexedDB
在浏览器中为你提供了一个功能强大的异步文档数据库。IndexedDB API 可能有点麻烦,但是像 Dexie 这样的库可以为你提供 IndexedDB
的包装器,使用起来要容易得多。