引用来自 MDN 上的定义:
IndexedDB 是一种低级 API,用于在客户端储存大量的结构化数据(也包括文件和二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。
由于 IndexedDB 的 API 比较复杂,和我们常用的操作数据的方法差异较大,所以 MDN 上还推荐了几个对开发者比较友好的的操作 IndexedDB 的库:localForage、dexie.js、PouchDB、idb、idb-keyval、jsStore、lovefieId,这些库有部分可能停止维护了。
IndexedDB 的兼容性
由上图可以看出市面上主流的浏览器已经支持 IndexedDB API 了(IE 浏览器不是主流浏览器,毕竟 IE 浏览器已经停用了)
IndexedDB 的特点
- 异步操作:IndexedDB 是基于事件的异步数据库。所有的数据库操作(包括打开数据库、操作数据、查询数据等)都是非阻塞的,这就意味着所有的数据库操作都不会阻塞主线程。
- 键值对存储:IndexedDB 内部采用对象仓库的形式存放数据,几乎所有类型的数据都可以直接保存到数据库中,包括 javascript 对象、文件、二进制数据等。
- 支持事务:意味着在同一组操作中执行多个存储或者检索操作时,只要有一步发生错误或者失败,整个操作都会被取消并回滚,从而确保数据的完整性。(要么全部成功,要么全部失败。)
- 同源限制:IndexedDB 受到同源限制,每个数据库都会和创建其的域名绑定,不能跨协议、端口、域名使用。
- 存储空间大:IndexedDB 的空间比 localStorage 大得多,一般来说不少于 250MB,甚至没有上限。
IndexedDB 的用途
虽然一般来说普通的网页很少会有储存大量数据的需求,但是不代表 IndexedDB 没有实际的用途,例如:
- 用户个性化配置:Web 应用程序可以使用 IndexedDB 来存储用户的偏好设置、个人配置和自定义选项,以便在用户下次访问时加载这些设置。
- 离线应用:离线版本的
ToDo List、读书软件、离线精美图库、备忘录。虽然很多人觉得这些功能可能比较鸡肋,但是对于一个前端来说,可以结合自己的能力开发一些自定义的网页应用还是挺好的。 - 地图优化:一些在线地图软件可以利用 IndexedDB 储存一些常用的静态资源,包括但是不限于天气背景、城市列表、坐标位置等。
- 网页游戏:一些网页游戏可以利用 IndexedDB 保存大量的图片资源、人物素材、NPC 坐标等。
- 图片编辑器:可以利用 IndexedDB 记录图片的编辑历史、图片草稿等,方便用于撤销和恢复。
以上只是对我认为的可能的用途整理了一下,但是实际用途肯定不仅仅是我列举的这些项。IndexedDB 尽管实际用途不大,甚至有时候直接使用 localStorage 做数据储存就够了,但是我们不能认为 IndexedDB 一无是处,毕竟它相当于localStorage还是有优势的,它的储存空间很大、几乎可以保存的所有的数据类型,记住 “存在即合理”。
IndexedDB 的使用
下文只展示了 IndexedDB 数据库的各种简单操作,更加完整的操作 API 请查看 MDN —— IndexedDB API
操作流程
- 打开并创建数据库
- 创建对象仓库(新建表)
- 启动事务对数据进行操作
数据库相关操作
创建/打开数据库【indexedDB.open()】
使用 indexedDB 的第一步就是使用 windoww.indexedDB.open() 方法创建/打开数据库。当数据库不存在时 indexedDB.open 可以创建数据库,但是该方法并不会立即创建/打开数据库,而是返回 IDBOpenDBRequest 对象,该对象有多个处理函数,我们需要在该事件的处理函数中创建数据库模式。接下来我们先看一段代码,然后再一一分析 open 方法的参数和回调,如下:
// 用于记录保存数据库实例(该实例会在下面多个函数内部使用)
let db = null;
// 打开或者创建数据库(示例代码,实际运用还需进一步封装)
function initIndexedDB() {
// 打开一个名为 school 的数据库(不存在就创建),版本号为 1
const request = window.indexedDB.open('school', 1);
// 数据库打开失败
request.onerror = (event) => {
// 可以通过 event.target.error 获取详细的报错文本
console.log('数据库打开失败', event);
};
// 数据库打开/创建成功
request.onsuccess = (event) => {
console.log('数据库打开/创建成功', event);
// db = request.result;
db = event.target.result; // 等同于上面的 db = request.result;
};
// 数据库升级
// 该回调只有在数据库创建和数据库升级(版本号改变)的时候触发
request.onupgradeneeded = (event) => {
console.log('数据库升级', event);
const database = event.target.result;
// 先判断对象仓库是否存在
if (!database.objectStoreNames.contains('student')) {
// 创建一个名为 student 的对象仓库,主键为 id
database.createObjectStore('student', { keyPath: 'id' });
}
if (!database.objectStoreNames.contains('teacher')) {
// 创建一个名为 teacher 的对象仓库,主键为 id
database.createObjectStore('teacher', { keyPath: 'id' });
}
};
// 数据库打开操作阻塞(中断)
request.onblocked = (event) => {
console.log('数据库打开操作阻塞(中断)', event);
};
}
看完上面代码你是否想问为什么创建数据库时候需要声明版本号?其实是因为在 indexedDB 中版本号是一种重要的数据库管理机制,在同一时刻只能存在一个版本的数据库,如果需要修改数据库的数据结构(包括增加对象仓库、删除对象仓库、增加索引、删除索引)都需要通过升级数据库版本号实现,因为修改数据结构的方法只能在 onupgradeneeded 处理函数中调用。
indexedDB.open() 方法接受两个参数,第一个参数是数据库的名称,第二个参数数据库的版本号,下面是基本的调用模板:
window.indexedDB.open(name, version);
- name:数据库的名称,必填,类型是字符串,可以为空字符串但是不建议。如果数据库不存在就创建并打开该数据库,如果存在就打开对应的数据库。
- version:数据库的版本号,非必填,类型为正整数(无符号整型:an unsigned long long number)。如果当前要打开数据库已存在,默认值就是当前数据库的版本号,否则默认值为 1。需要注意的是数据库的版本号必须要是正整数,如果版本号为负数或者 0 都会报错;而且版本号只能增加不能降低,也就是说如果已有同名数据库版本号为 2,再次使用 indexedDB.open 打开版本号为 1 的同名数据库就会触发错误处理函数。
- version < 0:当版本号为负数时浏览器会抛出异常:
Value is outside the 'unsigned long long' value range.。 - version === 0:当版本号为 0 时,浏览器也会抛出异常:
The version provided must not be 0.。 - version 是小数:当 version 是小数时,虽然浏览器不会报错,但是对应的值会向下取整(即例如:2.9 => 2)。
- version < 0:当版本号为负数时浏览器会抛出异常:
当 version 为小数时,我质疑 MDN中文页面 的翻译是错误的,上面提到
,但是我经过测试都是向下取整(测试环境:window 10, chrome 115)。不能使用浮点数,否则它将会被转变成离它最近的整数
上面提到 indexedDB.open() 返回的是一个 IDBOpenDBRequest 对象,该对象有多个处理函数,下面我对几个常用的处理函数简单说明一下:
- onsuccess:数据库创建/打开成功的回调。
- onerror:数据库创建/打开失败的回调,可以通过
event.target.error获取详细的报错信息(上面代码注释有写)。可能触发该回调的原因有:创建/打开的数据库版本号小于已有同名数据库版本号、在onupgradeneeded回调中抛出异常、内存不足等。 - onupgradeneeded:数据库版本升级回调,该回调只有在数据库版本升级时或者创建数据库时才会触发。所有的要修改数据库数据结构的方法都应该在此处调用,否则会报错。
- onblocked:数据库打开操作阻塞(中断)的回调。可能触发该回调的原因有:数据库未关闭再次打开该数据、其他页面正在操作该数据库的数据等
获取数据库列表【indexedDB.databases()】
可以通过 indexedDB.databases() 方法获取数据库列表,该方法返回的是一个 promise 对象,调用 promise 的 then 函数可以获取所有有效的数据库列表,列表中每条数据包括每个数据库的名称和版本号,如果没有数据库就返回空数组,下面是一个简单的例子:
// 获取数据库列表
function getIndexedDBList() {
indexedDB.databases().then(list => {
console.log(list); // [{name: 'school', version: 1}]
});
}
删除数据库【indexedDB.deleteDatabase()】
和 indexedDB.open() 一样,indexedDB.deleteDatabase() 方法返回的也是 IDBOpenDBRequest 对象。但是需要注意的是删除数据库前必须要确保数据库已经关闭(调用 db.close() 方法关闭数据库,上面有提到 db 是数据库实例),否则就会触发 onblocked 回调。
// 删除数据库
function removeIndexedDB() {
// 删除一个名为 school 的数据库
const request = window.indexedDB.deleteDatabase('school');
// 数据库删除失败
request.onerror = (error) => {
console.log('数据库删除失败', error);
};
// 数据库删除成功
request.onsuccess = (event) => {
console.log('数据库删除成功', event);
};
// 数据库删除操作中断
request.onblocked = (event) => {
console.log('数据库删除操作中断,请先关闭数据库连接!', event);
};
}
修改数据库信息
修改数据库信息包括修改数据库名称和修改数据库版本号,修改数据库版本号比较简单,只需要向上面那样调用 window.indexedDB.open() 事件修改为对应的版本号就可以了(修改前需要调用 db.close() 方法(db是数据库实例,最开始的代码注释中有提到)关闭数据库连接)。但是修改数据库名称并没有现成的方法,可以通过以下步骤来实现(代码就不写了,太长了,因为要考虑很多因素):
- 打开旧的数据库和新的数据库连接。
- 遍历旧的数据库,从旧的数据库中读取数据(需要考虑多对象仓库、多索引等情况)。
- 将旧数据库中的数据写到新的数据库中。
- 关闭新的和旧的数据库连接。
- 删除旧的数据库。
但是需要注意的是该操作性能消耗比较高,容易造成内存溢出,修改完要尽量关闭不必要的连接。
事务【IDBDatabase.transaction()】
IDBDatabase.transaction() 方法返回一个包含 IDBTransaction.objectStore 方法的事务对象 IDBTransaction,用于访问对象存储空间(注意:事务执行是一个微任务,因此请勿在创建事务实例后的宏任务外部再次使用事务实例)。以下是 IDBTransaction 对象的截图:
- 事务主要是为了保证数据完整性和唯一性。
- 所有读取和写入数据的操作,包括增删改查(添加、更新、删除数据)都必须在事务中执行。
- 事务完成后会自动结束,并触发
oncomplete回调,也可以调用transaction.abort()方法手动终止事务。
使用事务的语法如下:
IDBDatabase.transaction(storeNames[, mode[, options]])
- storeNames: 指定创建的新事务作用范围包括哪些对象仓库,类型为字符串数组(空数组会异常),必填。当数组只有一个元素时,可以为字符串;如果需要访问数据库中的所有对象存储空间,可以使用属性
db.objectStoreNames。如果对象仓库不存在就会报错。 - mode:指定事务打开的模式,非必填。常用的可选值如下:
- readonly: 只读事务,默认值。顾名思义,该模式下只能用户查询操作,不能用于增加、更新、删除等操作。
- readwirte:读写事务。
- versionchange:数据库版本变更模式。该模式在这里支持使用,否则会报错,当数据库创建、版本升级的时候自动使用。
- options: 事务的配置,非必填。options 对象包含以下属性:
// 创建一个读写事务
const transaction = db.transaction([storeName], 'readwrite');
对象仓库(表)相关操作
新建对象仓库(新建表)【IDBDatabase.createObjectStore()】
IDBDatabase.createObjectStore() 方法用于新建对象仓库,即新建数据库表,该操作只能在数据库更新的回调(上面提到的:request.onupgradeneeded)中调用,创建成功后返回对象仓库的实例。需要注意的是不能创建同名的对象仓库。
// request.onupgradeneeded 来自章节《创建/打开数据库【indexedDB.open()】》的代码中
request.onupgradeneeded = (event) => {
console.log('数据库升级', event);
const database = event.target.result;
// 创建一个名为 student 的对象仓库,主键为 id
database.createObjectStore('student', { keyPath: 'id' });
};
数据库可能会因为其他原因升级版本,这样就会导致多次调用 createObjectStore 方法,因此最好在创建前先判断是否已存在同名的对象仓库,否则会报错。
// request.onupgradeneeded 来自章节《创建/打开数据库【indexedDB.open()】》的代码中
request.onupgradeneeded = (event) => {
console.log('数据库升级', event);
const database = event.target.result;
let studentStore = null;
// 先判断对象仓库是否存在,
if (!database.objectStoreNames.contains('student')) {
// 创建一个名为 student 的对象仓库,主键为 id
studentStore = database.createObjectStore('student', { keyPath: 'id' });
}
};
数据在对象仓库中保存的形式并不是像 MySQL 那样的格式,而是以键值对的形式存在,如下图所示:
从上面代码中可以看出 IDBDatabase.createObjectStore() 方法接受两个参数,第一个参数用于指定创建的对象仓库名称,第二个参数是可选参数,是对象仓库的配置项,下面是该方法的语法:
IDBDatabase.createObjectStore(name, options);
- name:对象仓库名称,即表名,必填,字符串类型,可为空字符串。
- options:对象仓库的配置项,非必填,类型是对象,对象包括下面键:
- keyPath:单条数据的主键(主键不能重复),可以为字符串也可以是数组,默认为空。
- autoIncrement:是否自动将主键设置为自增,类型是布尔值,默认值为 false。
在 options 中 keyPath 为字符串时,autoIncrement 也是可以为 true 的;但是当 keyPath 为数组时,autoIncrement 是不能为 true 的,都则会报错。如果在调用 createObjectStore 方法时没有传 options 参数就需要在插入数据时声明主键。
假设我要往数据库中依次插入以下几条数据(假设插入失败后不影响后续执行):
{ id: 1, name: '张三' }
{ name: '李四', email: 'lisi@email.com' }
{ id: 2, name: '王五', email: 'zhangsan@email.com' }
{ id: 3, name: '赵六' }
{ id: 10, name: '李四', email: 'lisi@email.com' }
{ keyPath: 'id', autoIncrement: true }:第三条数据会插入失败,其他数据都可以正常插入,因为在第二条数据没有 id 属性,autoIncrement会自动给第二条数据增加一个主键 2 (插入该数据前,最大的组件为1,自增1为2),而第三条数据的主键刚好是 2,所以插入失败。{ keyPath: 'id' }:第二条数据插入失败(每条数据都要有主键)。{ keyPath: ['id', 'email'], autoIncrement: true }:报错,所有数据都无法插入,因为当 keyPath 为数组时,autoIncrement 是不能为 true。{ keyPath: ['name', 'email'] }:只有第二和第三条数据插入成功,其他数据都插入失败,因为第五条数据的name和email都与第二条一致,相当于主键相同了,所以插入失败。
获取查询对象仓库列表【IDBDatabase.objectStoreNames】
// 获取对象仓库列表
function getObjectStoreList() {
const list = db.objectStoreNames;
return list; // DOMStringList {0: 'student', 1: 'teacher', length: 2}
}
获取对象仓库的详细信息【IDBTransaction.objectStore()】
要查询对象仓库的配置信息需要用到事务,具体代码如下:
// 获取对象仓库的详细信息
function getObjectStoreConfig(name) {
// db 是数据库实例,最开始的代码注释中有提到
if (!db.objectStoreNames.contains(name)) {
console.error('对象仓库不存在');
return;
}
const transaction = db.transaction(name, 'readonly');
// 对象仓库实例(可获取详细信息)
const objectStore = transaction.objectStore(name);
// console.log('对象仓库的名称:', objectStore.name);
// console.log('对象仓库的主键:', objectStore.keyPath);
// console.log('对象仓库主键是否自增:', objectStore.autoIncrement);
return objectStore;
}
删除对象仓库(删除表)【IDBDatabase.deleteObjectStore()】
和创建对象仓库一样,删除对象仓库也需要在 request.onupgradeneeded 回调中调用,因为这也是修改了数据库的数据结构。
request.onupgradeneeded = (event) => {
const database = event.target.result;
// 先判断要删除的对象仓库是否存在(不存在删除会报错)
if (database.objectStoreNames.contains('student')) {
// 删除对应的对象仓库
database.deleteObjectStore('student');
}
};
修改对象仓库的信息
在IndexedDB中,一旦对象仓库(Object Store)被创建,就无法直接修改其配置信息,包括主键路径(keyPath)和是否自增(autoIncrement)等。这是因为IndexedDB要求对象仓库的结构在创建后就是不可更改的。如果要修改配置信息可以按照以下步骤完成:
- 创建一个新的对象仓库(不能和旧的同名,如果同名会报错),其中包含您想要的新配置信息。
- 从旧对象仓库中读取数据。
- 将数据插入到新的对象仓库中。
- 删除旧的对象仓库。
索引相关操作
在创建对象仓库时可以新建索引(声明数据中某些字段作为索引,从而提升查询效率)。新建索引并不是必须的,索引的存在可以极大地提高查询性能,特别是在处理大量数据时。使用适当的索引,可以更快地检索数据并加速数据库操作。但是,索引也会增加数据库的存储空间和更新开销,因此需要根据具体应用场景和数据查询需求来选择是否创建索引和创建索引的个数。
由于 IndexedDB 的存储是键值对存储,数据库本身并不在乎保存的数据的数据结构是怎么样的,它们在数据库中也只是一个 value 的值,为了提升查询效率建议给可能会被检索的字段新建索引。
为了更加方便理解索引,我截了一张图,截图中 student 对象仓库(表)中创建了两个索引:name 和 email。
创建索引【IDBObjectStore.createIndex()】
和 IDBDatabase.createObjectStore() 一样, IDBObjectStore.createIndex() 方法也是只能在数据库更新的回调(上面提到的:request.onupgradeneeded)中调用,一般会在创建对象仓库(createObjectStore)的时候就创建索引。创建索引的语法如下:
IDBObjectStore.createIndex(indexName, keyPath[, options]);
具体的示例代码:
// request.onupgradeneeded 来自章节《创建/打开数据库【indexedDB.open()】》的代码中
request.onupgradeneeded = (event) => {
console.log('数据库升级', event);
const database = event.target.result;
// 先判断对象仓库是否存在
if (!database.objectStoreNames.contains('student')) {
// 创建一个名为 student 的对象仓库,主键为 id
const studentStore = database.createObjectStore('student', { keyPath: 'id' });
// 创建索引名为 name 的索引,取数据中 name 字段的值作为索引值
studentStore.createIndex('name', 'name');
// 创建索引名为 email 的索引,取数据中 email 字段的值作为索引值,且该索引唯一
studentStore.createIndex('email', 'email', { unique: true });
studentStore.createIndex('age', 'age');
}
if (!database.objectStoreNames.contains('teacher')) {
// 创建一个名为 teacher 的对象仓库,主键为 id
database.createObjectStore('teacher', { keyPath: 'id' });
}
};
参数说明:
- indexName:索引名称,可以为空字符串。
- keyPath:这是一个字符串或字符串数组,用于指定索引所使用的键路径。索引可以根据一个或多个对象属性的值来创建。键路径可以是对象属性的名称,也可以是深层次的嵌套属性,用点号来连接。
- options:这是一个包含索引配置选项的对象,options 对象包含以下属性:
unique:布尔值,默认为false,表示索引中的键是否是唯一的。如果设置为true,则表示不允许有相同的键值(即该对象仓库中每条数据中的 keyPath 键对应的值必须唯一)。multiEntry:布尔值,默认为false,表示如果 keyPath 字段指向是数组或者对象时,是否为每个值创建一个单独的索引项。multiEntry为true时,keyPath 不能是数组类型,否则会报错。
获取索引列表【IDBObjectStore.indexNames】
// 获取索引列表
function getIndexList(storeName) {
// db 是数据库实例,最开始的代码注释中有提到
if (!db.objectStoreNames.contains(storeName)) {
console.error('对象仓库不存在');
return;
}
const transaction = db.transaction(storeName, 'readonly');
// 对象仓库实例
const objectStore = transaction.objectStore(storeName);
return objectStore.indexNames;
}
console.log(getIndexList('student')); // DOMStringList {0: 'age', 1: 'email', 2: 'name', length: 3}
获取索引的详细信息【IDBObjectStore.index()】
// 获取索引的详细信息
function getIndexConfig(storeName, indexName) {
// db 是数据库实例,最开始的代码注释中有提到
if (!db.objectStoreNames.contains(storeName)) {
console.error('对象仓库不存在');
return;
}
const transaction = db.transaction(storeName, 'readonly');
// 对象仓库实例
const objectStore = transaction.objectStore(storeName);
if (!objectStore.indexNames.contains(indexName)) {
return null;
}
return objectStore.index(indexName);
}
console.log(getIndexConfig('student', 'name'));
// IDBIndex {name: 'name', objectStore: IDBObjectStore, keyPath: 'name', multiEntry: false, unique: false}
删除索引【IDBObjectStore.deleteIndex()】
删除索引也是改变数据库的数据结构的一种形式,所以该方法也只能在数据库更新的回调(上面提到的:request.onupgradeneeded)中调用。
if (studentStore.indexNames.contains('tid')) {
studentStore.deleteIndex('tid');
}
修改索引信息
在 IndexedDB 中,一旦创建了对象存储空间(IDBObjectStore)中的索引(IDBIndex),就无法直接修改索引的配置或属性。如果需要修改索引信息就需要先 删除旧的索引 再 创建新的索引。
数据相关操作
所有的和数据操作相关的方法都需要用到事务,而且事务的模式必须是 readwrite。
新增数据【IDBObjectStore.add()】
IDBObjectStore.add() 用于新增数据,语法:
IDBObjectStore.add(data[, key]);
如果在创建对象仓库时(createObjectStore)没有声明 keyPath,而且 autoIncrement 也不为 true 的话,这里传 key 才有用,否则会报错。这里的 key 用于就是数据的主键。
插入单条数据
// 往数据仓库中写入单条数据
function insertSingleData(storeName, data) {
if (!db.objectStoreNames.contains(storeName)) {
console.error('对象仓库不存在');
return;
}
// 创建一个读写事务
const transaction = db.transaction([storeName], 'readwrite');
transaction.oncomplete = (event) => {
console.log('事务执行完成', event);
};
transaction.onerror = (event) => {
console.log('事务执行错误', event.target.error);
};
transaction.onabort = (event) => {
console.log('事务执行中断', event.target.error);
};
// 获取对象仓库
const objectStore = transaction.objectStore(storeName);
// 执行数据写入操作
const request = objectStore.add(data);
// 监听数据写入成功事件
request.onsuccess = (event) => {
console.log('数据写入成功', event);
};
// 监听数据写入失败事件
request.onerror = (event) => {
console.log('数据写入失败', event.target.error);
};
}
插入多条数据
下面代码,在写入多条数据时使用了事务,因此只要有一条数据写入失败都会导致所有的数据无法正常写入,因为这是在同一个事务里面的一组操作,事务失败就会回滚。
// 往数据仓库中写入多条数据
function insertMultipleData(storeName, data) {
if (!db.objectStoreNames.contains(storeName)) {
console.error('对象仓库不存在');
return;
}
const objectStore = db.transaction([storeName], 'readwrite')
.objectStore(storeName);
data.forEach(item => {
const request = objectStore.add(item);
request.onsuccess = (event) => {
console.log('数据写入成功', event);
};
request.onerror = (event) => {
console.log('数据写入失败', event.target.error);
};
});
}
查询数据
在查询数据之前假设我们已经对象仓库中插入下面数据了:
insertSingleData('student', { id: 1, name: '张三', age: 18, tid: [1, 2, 3] });
insertSingleData('student', { id: 2, name: '李四', age: 19, email: 'lisi@email.com', tid: [1, 4] });
insertSingleData('student', { id: 3, name: '王五', age: 20, email: 'zhangsan@email.com', tid: [3, 4] });
insertSingleData('student', { id: 4, name: '赵六', age: 21, email: 'zhaoliu@email.com', tid: [2, 4] });
insertSingleData('student', { id: 5, name: '张三', age: 22, tid: [1, 2, 3, 4, 5] });
insertMultipleData('teacher', [
{ id: 1, name: '刘校长' },
{ id: 2, name: '谢书记' },
{ id: 3, name: '秦老师' },
{ id: 4, name: '黄老师' },
{ id: 5, name: '康老师' },
]);
获取整个表所有数据【IDBObjectStore.getAll()】
// 获取整个表所有数据(假设对象仓库存在,否则要判断)
function getAllDataFormStore(storeName) {
const request = db.transaction([storeName], 'readwrite')
.objectStore(storeName).getAll();
request.onsuccess = (event) => {
console.log('数据读取成功', event.target.result);
};
request.onerror = (event) => {
console.log('数据读取失败', event.target.error);
};
}
根据主键查询数据【IDBObjectStore.get(key)】
根据组件查询数据是最简单的数据查询方法,直接使用对象仓库的 get 方法就可以了。
// 根据主键查询数据
function findDataByKey(storeName, key) {
if (!db.objectStoreNames.contains(storeName)) {
console.error('对象仓库不存在');
return;
}
const objectStore = db.transaction([storeName], 'readwrite').objectStore(storeName);
const request = objectStore.get(key);
request.onsuccess = (event) => {
console.log('数据读取成功', event.target.result);
};
request.onerror = (event) => {
console.log('数据读取失败', event.target.error);
};
}
findDataByKey('student', 1); // { id: 1, name: '张三', age: 18, tid: [1, 2, 3] }
根据索引查询数据(单条)
// 根据索引查询数据(假设对象仓库、索引存在,否则要判断)
function findDataByIndex(storeName, indexName, queryValue) {
const index = db.transaction([storeName], 'readwrite')
.objectStore(storeName).index(indexName);
const request = index.get(queryValue);
request.onsuccess = (event) => {
console.log('数据读取成功', event.target.result);
};
request.onerror = (event) => {
console.log('数据读取失败', event.target.error);
};
}
findDataByIndex('student', 'email', 'lisi@email.com');
// 结果:{ id: 2, name: '李四', age: 19, email: 'lisi@email.com', tid: [1, 4] }
查询满足条件的数据(多条)
如果想要查询多条数据,就需要用到 IDBKeyRange 对象,还会用到 openKeyCursor() 和 openCursor() 方法,下面先简单介绍一下这三者的用法。
IDBKeyRange 对象
IDBKeyRange 是 IndexedDB API 中的一个对象,用于定义和表示键范围,用于在对象存储空间中执行数据检索操作。它允许您指定一定范围内的键,从而可以更精确地检索数据。IDBKeyRange 对象不可变,一旦创建,就不能更改其值。IDBKeyRange 对象有几个方法可以用来创建不同类型的键范围:
- IDBKeyRange.only(key):创建一个只包含一个键的范围,相当于精确搜索(即:===)。只会匹配与指定键相等的条目。
- IDBKeyRange.lowerBound(lower[, open]): 创建一个从指定键值开始的范围,通常用于范围搜索,相当于大于等于(或大于)。如果
open参数为true,则不包括指定键值,如果为false,则包括指定键值,默认值为false。(例如查询年龄大于等于20岁的学生:IDBKeyRange.lowerBound(20)) - IDBKeyRange.upperBound(upper[, open]): 创建一个以指定键值结束的范围,通常用于范围搜索,相当于小于等于(或小于)。与
lowerBound类似,open参数决定是否包括指定键值。 - IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen): 相当于第2和第3两种方法的结合版本,主要用于范围搜索。(例如查询年龄在18-22岁的学生:IDBKeyRange.bound(18, 22))
下面是一个简单的例子:
// 根据索引查询所有满足条件的数据(假设对象仓库、索引存在,否则要判断)
function findAllDataByIndex() {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['student'], 'readwrite');
const objectStore = transaction.objectStore('student');
const index = objectStore.index('age');
// const queryRange = IDBKeyRange.only(20); // 查询年龄刚好是 20 岁的学生
// const queryRange = IDBKeyRange.lowerBound(20); // 查询年龄大于等于 20 岁的学生(包括 20 岁)
// const queryRange = IDBKeyRange.lowerBound(20, true); // 查询年龄大于 20 岁的学生(不包括 20 岁)
// const queryRange = IDBKeyRange.upperBound(20); // 查询年龄小于等于 20 岁的学生(包括 20 岁)
const queryRange = IDBKeyRange.bound(18, 22, false, true); // 查询年龄在 18-22 岁的学生(包括18,但是不包括22)
// 用于保存满足条件的数据
const result = [];
// 查询请求(正向排序:从小到大)
const request = index.openCursor(queryRange);
// 查询请求(正向排序:从小到大)
// const request = index.openCursor(queryRange, 'next');
// 查询请求(反向排序:从大到小)
// const request = index.openCursor(queryRange, 'prev');
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const data = cursor.value;
result.push(data);
// 移动到下一个数据项
cursor.continue();
}
};
request.onerror = (event) => {
console.log('数据读取失败', event.target.error);
};
transaction.oncomplete = (event) => {
console.log('事务执行完成', event);
resolve(result);
};
transaction.onerror = (event) => {
console.log('事务执行错误', event.target.error);
reject(event.target.error);
};
transaction.onabort = (event) => {
console.log('事务执行中断', event.target.error);
reject(event.target.error);
};
});
}
openCursor 方法
openCursor 方法是 IndexedDB API 中的一个方法,用于在一个对象存储空间中打开一个游标(cursor),以便遍历存储在该对象存储空间中的数据。游标是一种用于遍历数据库中数据的方式,类似于数据库中的迭代器或指针。
语法如下:
IDBObjectStore.openCursor();
IDBObjectStore.openCursor(range);
IDBObjectStore.openCursor(range, direction);
IDBIndex.openCursor();
IDBIndex.openCursor(range);
IDBIndex.openCursor(range, direction);
具体的使用示例可以看上一小节的代码示例,主要用于遍历数据。direction 参数用于指定游标遍历的方向,可能的值:
next:默认值,表示游标将按照升序顺序遍历数据。游标将从存储中的第一个数据项开始,然后依次向后移动。prev:表示游标将按照降序顺序遍历数据。游标将从存储中的最后一个数据项开始,然后依次向前移动。
openKeyCursor 方法
IDBObjectStore.openKeyCursor() 方法是在 IndexedDB 中用于打开一个键游标(KeyCursor)的方法。键游标是一种能够遍历对象存储(Object Store)中的键(Key)的方式,而不需要获取完整的对象值。这在需要只访问键而不需要完整对象数据的情况下非常有用,因为它可以提高性能和减少资源消耗。
语法如下:
IDBObjectStore.openKeyCursor();
IDBObjectStore.openKeyCursor(range);
IDBObjectStore.openKeyCursor(range, direction);
IDBIndex.openKeyCursor();
IDBIndex.openKeyCursor(range);
IDBIndex.openKeyCursor(range, direction);
direction 参数用于指定游标遍历的方向,可能的值:
next: 游标按照键的升序顺序遍历。nextunique: 类似于next,但会跳过重复的键值。prev: 游标按照键的降序顺序遍历。prevunique: 类似于prev,但会跳过重复的键值。
联表查询
// 联表查询
function linkedDataSearch() {
return new Promise((resolve) => {
const transaction = db.transaction(['student', 'teacher'], 'readwrite');
const studentStore = transaction.objectStore('student');
const teacherStore = transaction.objectStore('teacher');
const index = studentStore.index('name');
const queryRange = IDBKeyRange.only('张三');
const result = [];
const request = index.openCursor(queryRange);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const data = cursor.value;
data.teachers = [];
const tid = data.tid;
for (let i = 0; i < tid.length; i++) {
const teacherRequest = teacherStore.get(tid[i]);
teacherRequest.onsuccess = (event) => {
data.teachers.push(event.target.result);
};
}
result.push(data);
cursor.continue();
}
};
transaction.oncomplete = (event) => {
console.log('事务执行完成', event);
resolve(result);
};
});
}
删除数据
清空整个表所有数据【IDBObjectStore.clear()】
// 清空整个表所有数据(假设对象仓库存在,否则要判断)
function clearObjectStore(storeName) {
const request = db.transaction([storeName], 'readwrite')
.objectStore(storeName).clear();
request.onsuccess = (event) => {
console.log('对象仓库清空成功', event.target.result);
};
request.onerror = (event) => {
console.log('对象仓库清空失败', event.target.error);
};
}
根据主键删除数据【IDBObjectStore.delete(key)】
// 根据主键删除数据(假设对象仓库存在,否则要判断)
function deleteDataByKey(storeName, key) {
const transaction = db.transaction([storeName], 'readwrite');
const objectStore = transaction.objectStore(storeName);
const request = objectStore.delete(key);
request.onsuccess = (event) => {
console.log('数据删除成功', event);
};
request.onerror = (event) => {
console.log('数据删除失败', event.target.error);
};
}
根据索引删除数据【IDBCursor.delete()】
// 删除年龄大于等于20岁的学生
// 根据索引删除所有满足条件的数据(假设对象仓库、索引存在,否则要判断)
function deleteData() {
const transaction = db.transaction(['student'], 'readwrite');
const objectStore = transaction.objectStore('student');
const index = objectStore.index('age');
const queryRange = IDBKeyRange.lowerBound(20);
const request = index.openCursor(queryRange);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
cursor.delete();
cursor.continue();
}
};
request.onerror = (event) => {
console.log('数据删除失败', event.target.error);
};
}
修改数据
根据主键修改数据【IDBObjectStore.put(data)】
IDBObjectStore.put() 可用于修改数据,也可用于插入新的数据(当主键不存在时就插入新的数据)。
// 根据主键修改数据(假设对象仓库存在,否则要判断)
function updateDataByKey(storeName, data) {
const transaction = db.transaction([storeName], 'readwrite');
const objectStore = transaction.objectStore(storeName);
// 如果数据的主键不存在就相当于插入一条新的数据
const request = objectStore.put(data);
request.onsuccess = (event) => {
console.log('数据修改成功', event);
};
request.onerror = (event) => {
console.log('数据修改失败', event.target.error);
};
}
// 清空 id 为 5 的学生的老师列表(假设数据已存在)
updateDataByKey('student', { id: 5, name: '张三', age: 22, tid: [] });
// 插入一条新的数据(假设数据库中不存在id为6的学生数据)
updateDataByKey('student', { id: 6, name: '张三', age: 22, tid: [] });
根据索引修改数据【IDBCursor.update(data)】
// 将所有学生名为张三的学生的姓名修改为张四(需求太鸡肋了,哈哈,这里只是为了演示,请勿当真)
// 根据主键修改数据(假设对象仓库、索引存在,否则要判断)
function updateData() {
const transaction = db.transaction(['student'], 'readwrite');
const objectStore = transaction.objectStore('student');
const index = objectStore.index('name');
const queryRange = IDBKeyRange.only('张三');
const request = index.openCursor(queryRange);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const data = cursor.value;
data.name = '张四';
cursor.update(data);
cursor.continue();
}
};
request.onerror = (event) => {
console.log('数据修改失败', event.target.error);
};
}
IndexedDB、Cookie、localStorage、sessionStorage 的对比
下面是 IndexedDB、Cookie、localStorage、sessionStorage 的对比:
| 特点 | 存储容量 | 生命周期 | 跨会话 | 目的 |
|---|---|---|---|---|
| IndexedDB | 通常较大 | 永久存储 | 否 | 存储大量结构化数据,如离线应用、缓存数据等 |
| Cookie | 通常几KB | 可设置过期时间 | 是 | 在客户端和服务器间传递少量信息,如用户认证信息、会话标识等 |
| localStorage | 通常几兆字节 | 永久存储 | 是 | 长期保存用户数据,如偏好设置、登录状态等 |
| sessionStorage | 通常几兆字节 | 当前会话期间 | 否 | 临时保存会话期间数据,如表单传递 |
总结:
- 如果你需要在不同会话间共享数据,使用
localStorage或Cookie。 - 如果你需要在当前会话期间共享数据,使用
sessionStorage。 - 如果你需要在客户端存储较大的结构化数据,使用
IndexedDB。