MDN:IndexedDB 是一种底层异步 API,浏览器内置的数据库,用于在客户端存储大量的结构化数据(也包括文件/二进制大对象 blob)。
IndexedDB 是多年来引入浏览器的存储功能之一。它是一种键/值存储(noSQL 数据库,非关系型的数据库),被认为是在浏览器中存储数据的最终解决方案。
它是一个异步 API,这意味着执行昂贵的操作不会阻塞 UI 线程为用户提供草率的体验。它可以存储无限量的数据,但一旦超过某个阈值,用户会被提示给网站更高的限制。
所有现代浏览器都支持 IndexedDB。
它支持事务、版本控制并提供良好的性能。
在浏览器内部,我们还可以使用:
- Cookie — 可以承载非常少量的字符串(容量 ~4kb)
- Web Storage(或 DOM Storage),通常用于标识
localStorage
和sessionStorage
这两个键/值存储的术语。sessionStorage
不保留数据,会话结束时清除数据,localStorage
保留跨会话的数据,不主动清除,永远保留数据。
localStorage
和 sessionStorage
的缺点是存储大小有限且不一致,浏览器实现为每个网站提供 2MB 到 10MB 的存储空间。
过去我们也有 Web SQL,它是 SQLite 的包装器,但现在一些现代浏览器已弃用和不支持它,它从未成为公认的标准,因此不应该使用它。
虽然技术上可以为每个网站创建多个数据库,但通常只创建一个数据库,并且在该数据库中可以创建多个对象库(Object Store),可以理解为一张张表。
数据库对域来说是私有的,因此任何其他网站都无法访问另一个网站 IndexedDB 存储。
存储包含许多具有唯一键的项,该键表示可以识别对象的方式。
您可以通过执行添加、编辑和删除操作,并迭代它们所包含的项,使用事务更改这些存储。
总结一下上面介绍的 IndexedDB 特点:
- 储存空间大 — 相比于其他本地存储,IndexedDB 存储空间几乎无限量,但一旦超过某个阈值,用户会被提示给网站更高的限制。
- 支持二进制储存 — IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。
- 同源限制 — 数据库对域来说是私有的,因此任何其他网站都无法访问另一个网站 IndexedDB 存储。
- 异步 — 使用 IndexedDB 执行的操作是异步执行的,以免阻塞应用。
- 支持事务 — 执行一系列操作,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 键值对储存 — IndexedDB 内部采用对象库(object store)存放数据。所有类型的数据都可以直接存入,包括字符串、数字、对象、数组、日期。对象库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是唯一的,不能有重复,否则会抛出一个错误。
IndexedDB API 功能强大,但对于简单的情况可能看起来太复杂如果你更喜欢一个简单的 API,请尝试:
- localForage — 一个简单的 Polyfill,提供了简单的客户端数据存储的值语法。它在后台使用 IndexedDB,并且在不支持 IndexedDB 的浏览器中回退到 WebSQL 或 localStorage。
- Dexie.js — IndexedDB 的包装,通过简单的语法,可以进行代码开发。
- pouchdb — 使用 IndexedDB 在浏览器中实现 CouchDB 的客户端。
- idb — 一个微小的(〜115k)库,大多数 API 类似,但做了一些细微的改进,让数据库的空间有了很大的提升。
- idb-keyval — 使用 IndexedDB 实现的超级简单且小巧的键(~600B)基于 Promise 对值存储。
- JsStore — 一个 SQL 语法的 IndexedDB 包装器。
- lovefield — Lovefield Web App 的关系数据库,使用安全的 JavaScript 编写环境,可以用于不同的浏览中运行,类似 SQL 的 API,速度快、方便易用。
- MiniMongo — 由 localstorage 支持的客户端内存中的 mongodb,通过 http 进行服务器同步。MeteorJS 使用 MiniMongo。
这些库使 IndexedDB 对开发者来说更加友好。
下面我将使用 idb 库提供使用示例,如果您想了解原生 IndexedDB API 操作可以阅读阮一峰老师的浏览器数据库 IndexedDB 入门教程,其中可以了解一些未在本文中涉及的基本概念(很重要)。
最简单的方法是使用 CDN unpkg:
<script type="module">
import { openDB, deleteDB } from 'https://unpkg.com/idb?module'
</script>
在使用 IndexedDB API 之前,请始终确保在浏览器中检查支持,即使它广泛可用,但您永远不知道用户正在使用的是哪种浏览器:
;(() => {
'use strict'
if (!('indexedDB' in window)) {
console.warn('IndexedDB not supported')
return
}
// ...IndexedDB code
})()
接着,使用 openDB()
创建数据库:
;(async () => {
// ...
const dbName = 'mydbname'
const storeName = 'store1'
const version = 1 // 版本从 1 开始(默认)
const db = await openDB(dbName, version, {
upgrade(db, oldVersion, newVersion, transaction) {
const store = db.createObjectStore(storeName)
}
})
})()
前两个参数是数据库名称和版本号。第三个参数(可选)是一个对象,它包含一个仅在版本号高于当前安装的数据库版本时才调用的 upgrade
函数。在函数体中,您可以升级数据库的结构(对象库和索引)。
可以使用对象库的 put
方法,但首先我们需要一个对它的引用,可以从 db.createObjectStore()
中获取。
使用 put
时,值是第一个参数,键是第二个参数。这是因为如果在创建对象库时指定 keyPath
,则不需要在每个 put()
请求中输入键名,只需写入值即可。
这将在我们创建 store0
后立即填充它:
;(async () => {
//...
const dbName = 'mydbname'
const storeName = 'store0'
const version = 1
const db = await openDB(dbName, version, {
upgrade(db, oldVersion, newVersion, transaction) {
const store = db.createObjectStore(storeName)
store.put('Hello world!', 'Hello')
}
})
})()
要在以后添加项目,您需要创建一个读/写事务,以确保数据库的完整性(如果操作失败,事务中的所有操作都将回滚,状态将回到已知状态)。
为此,使用对调用 openDB
时获得的 dbPromise
对象的引用,然后运行:
;(async () => {
//...
const dbName = 'mydbname'
const storeName = 'store0'
const version = 1
const db = await openDB(/* ... */)
const tx = db.transaction(storeName, 'readwrite')
const store = await tx.objectStore(storeName)
const val = 'hey!'
const key = 'Hello again'
const value = await store.put(val, key)
await tx.done
})()
使用 get()
方法从对象库中获取一项:
const key = 'Hello again'
const item = await db.transaction(storeName).objectStore(storeName).get(key)
使用 getAllKeys
方法获取存储的所有键:
const items = await db
.transaction(storeName)
.objectStore(storeName)
.getAllKeys()
使用 getAll
方法获取所有存储的值:
const items = await db.transaction(storeName).objectStore(storeName).getAll()
删除数据库、对象库和数据。
使用 deleteDB()
方法删除整个 IndexedDB 数据库:
const dbName = 'mydbname'
await deleteDB(dbName)
使用事务删除对象库中的数据:
;(async () => {
//...
const dbName = 'mydbname'
const storeName = 'store1'
const version = 1
const db = await openDB(dbName, version, {
upgrade(db, oldVersion, newVersion, transaction) {
const store = db.createObjectStore(storeName)
}
})
const tx = await db.transaction(storeName, 'readwrite')
const store = await tx.objectStore(storeName)
const key = 'Hello again'
await store.delete(key)
await tx.done
})()
const name = 'mydbname'
const version = 1
openDB(name, version, {
upgrade(db, oldVersion, newVersion, transaction) {
console.log(oldVersion)
}
})
在这个回调中,您可以检查用户正在更新哪个版本,并相应地执行一些操作。
可以使用以下语法从以前的数据库版本执行迁移:
;(async () => {
//...
const dbName = 'mydbname'
const storeName = 'store0'
const version = 1
const db = await openDB(dbName, version, {
upgrade(db, oldVersion, newVersion, transaction) {
switch (oldVersion) {
case 0: // 之前未创建数据库
// 版本 1 中引入的对象库
db.createObjectStore('store1')
case 1:
// 版本 2 中的新对象库
db.createObjectStore('store2', { keyPath: 'name' })
}
db.createObjectStore(storeName)
}
})
})()
如案例 1 所示,createObjectStore()
接受指示数据库索引键的第二个参数。当您存储对象时,这非常有用:put()
调用不需要第二个参数,但可以只获取值(对象),键将映射到具有该名称的对象属性。
索引为您提供了一种稍后通过该特定键检索值的方法,并且它必须是唯一的(每个项必须具有不同的键)
可以将键设置为自动递增,因此不需要在客户端代码上跟踪它:
db.createObjectStore('notes', { autoIncrement: true })
如果值尚未包含唯一键(例如,如果收集的电子邮件地址没有关联名称),请使用自动递增。
您可以通过调用 objectStoreNames()
方法来检查对象库是否已存在:
const storeName = 'store1'
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName)
}
以上内容仅仅是一些基础知识和 API 的基本用法。IndexedDB 还有很多高级的内容可以讨论。
但本文到此就结束了,我希望这是一个良好的开端。
更多内容往下看。
- 原生 IndexedDB API 操作可以看看阮一峰老师的浏览器数据库 IndexedDB 入门教程
- How to Store Unlimited* Data in the Browser with IndexedDB
- IndexedDB