基本操作
前言
IndexedDB 是属于游览器的一个数据库。在游览器上有两种数据库:WebSQL 和 IndexedDB 数据库。但是现在 WebSQL 数据库已经就基本上废弃了
MDN 官网对于 IndexedDB 的解释:
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。
简单来说 IndexedDB 就是主要用来让客户端存储大量数据而生的,相对于 Cookie、SessionStorage、LocalStorage 等存储方式相比,它没有存储大小的限制
使用场景
在使用 IndexedDB 数据库的前提下是要确定将要存储大量的数据
- 变动性不强的数据可视化界面。大量数据每次请求都会让服务器造成很大的开销
- 即使通讯,把大量的聊天信息保存到本地
- 其他的存储方式不满足的情况下
特点
- 非关系型数据库(以键值对的形式存储数据)
- 持久化存储(像是 Cookie、SessionStorage、LocalStorage 在清除游览器缓存之后数据就会丢失,但是 IndexedDB 不会,除非手动删除数据库)
- 支持事务(跟 MySQL 一样,要么全部成功,否则只要有一步失败那么整个事务都会取消,数据回滚)
- 异步操作(IndexedDB 是异步操作的,他不会锁死游览器。LocalStorage是同步的)
- 同源策略(网页只能访问自身域名下的数据库,不能够跨域访问)
- 存储量大
相关概念
仓库(ObjectStore):IndexedDB 没有表的概念,他只有仓库(Store)的概念,可以把它理解成表,即一个仓库 = 一张表
索引(index):跟 MySQL 一样,都会为了加快数据的查找速率。可以在创建 仓库 的时候同时创建索引。在给某个字段设置索引之后,这个字段便 不能为空。如果不用索引,查询时代码就要查询出全部记录,然后对每个记录进行判断过滤,如果用索引,就能直接按指定参数查出指定条件的全部记录
游标(Cursor):游标是 IndexedDB 中的概念。可以理解为一个指针,如果要查询满足一个条件的所有数据时,就需要让游标一行一行往下走,游标走到的地方会返回一行数据。此时就可以对这行数据进行判断是否满足条件。可以同一时间拥有多个游标
IndexedDB 只能通过主键、索引、游标方式查询数据
事务:IndexedDB 数据库所有针对仓库的操作都是基于事务的
数据库相关操作
创建/连接
/**
* 打开数据库
* @param {object} dbName 数据库的名字
* @param {string} version 数据库的版本
* @return {object} 该函数会返回一个数据库实例
*/
function openDB1(dbName, version = 1) {
return new Promise((resolve, reject) => {
// 兼容性获取 indexedDB 对象
const indexedDB =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB;
if (!indexedDB) {
return reject(new Error('当前浏览器不支持 IndexedDB'));
}
// 打开数据库
const request = indexedDB.open(dbName, version);
// 定义数据库实例和事务
let db;
// 打开成功回调
request.onsuccess = (event) => {
db = event.target.result;
console.log(`数据库 [${dbName}] 打开成功`);
resolve(db); // 返回可用的数据库实例
};
// 打开失败回调
request.onerror = (event) => {
reject(event.target.error || new Error('数据库打开失败'));
};
// 数据库升级回调
request.onupgradeneeded = (event) => {
// 巴拉巴拉......
};
});
}
indexedDB.open(dbName, version)
这一段后面的 Version 是代表数据库的版本,每当更新数据库的时候版本也要发生变化,可以理解为应用的版本号
版本(Version)
IndexedDB 数据库都有一个版本号(version
),这是一个非负整数,用于确保数据库结构的更新和迁移能够平稳进行。当第一次创建数据库时,如果没有明确指定版本号,那么默认版本号为1。每次版本升级都会触发 onupgradeneeded
事件
const db = indexedDB.open("myDatabase", 1); // 打开 myDatabase 数据库,版本为 1
- 如果数据库不存在则会触发
onupgradeneeded
事件创建一个新的数据库 - 如果数据库存在,并且:
- 传递的版本号 == 数据库的版本号:打开数据库
- 传递的版本号 > 数据库的版本号:更新数据库
- 传递的版本号 < 数据库的版本号:报错
个人理解:数据库结构通过 js 代码创建出来并保存到游览器中。在之后的程序升级后可能现有的数据库结构不能够满足当前系统,所以要对当前数据库进行结构升级。在打开数据库的时候传递版本,当版本不一样时触发
onupgradeneeded
事件来达到自动升级的目的
升级
const request = indexedDB.open('myDatabase', 3);
// 数据库升级回调
request.onupgradeneeded = (event) => {
db = event.target.result;
const oldVersion = event.oldVersion; // 老版本号
let objectStore; // 存储对象
// 渐进式升级
// 第一次初始化数据
if (oldVersion < 1) {
objectStore = db.createObjectStore('user', {
keyPath: 'userId',
autoIncrement: true
});
// 创建索引
objectStore.createIndex('userId', 'userId', {unique: true}); // 唯一索引
objectStore.createIndex('name', 'name');
objectStore.createIndex('phone', 'phone');
objectStore.createIndex('namePhone', ['name', 'phone']); // 复合索引
}
// 第一次更新
if (oldVersion < 2) {
objectStore ||= db.transaction.objectStore('user');
// 删除 phone 索引
objectStore.deleteIndex('phone')
// 添加 age 索引
objectStore.createIndex('age', 'age');
}
// 第二次更新
if (oldVersion < 3) {
objectStore ||= db.transaction.objectStore('user');
// 删除 age 索引
objectStore.deleteIndex('age')
}
console.log(`数据库版本升级至 ${version}`);
};
下面是存储内容结构:
const user = {
userId: 1,
name: 'John Doe',
email: {
qq: '9527114541@qq.com'
// 其他邮箱
},
age: 30,
phone: '1234567890',
// 其他字段
}
通过判单老版本号来进行渐进式升级,就是先判断老版本号是在什么阶段。如果老版本号是 1,那么就从 oldVersion < 2
的阶段开始升级
删除
/**
* 删除数据库
* @param dbName 数据库的名字
* @returns {Promise<unknown>} Promise
*/
function deleteDB(dbName) {
return new Promise((resolve, reject) => {
// 兼容性获取 indexedDB 对象
const indexedDB =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB;
// 删除数据库
const request = indexedDB.deleteDatabase(dbName);
// 删除成功
request.onsuccess = () => resolve()
// 删除失败
request.onerror = (event) => reject(event.target.error)
})
}
仓库相关操作
创建仓库
const objectStore = db.createObjectStore('storeName', {
keyPath: 'id', // 主键
autoIncrement: true // 是否自增
})
通过 db 实例调用 createObjectStore
方法来创建仓库。仓库创建完成后会返回一个操作该仓库的对象,可以通过这个对象来进行**[创建/删除]索引**、CRUD数据等操作
创建索引
在创建完仓库后会返回一个对象。拿着这个对象创建索引,使用 createIndex
方法,接收三个参数
- 索引名称
- 键路径
- 选项对象
索引名称(indexName)
索引的名称,用来标识该索引。必须是唯一的,不能和其他索引或者对象存储空间的名称冲突
键路径(keyPath)
它有两种写法:纯字符串和字符串数组。
纯字符串(单索引):取顶级的值,如上的 name
字符串数组(复合索引):同样是取顶级的值,但是可以取多个,如上面的 namePhone
IndexedDB 没有嵌套索引,所有的索引都必须是顶级的,比如:
const user = { userId: 1, // ... email: { qq: '9527114541@qq.com' // ... }, // ... }
按照以上创建索引的方式
xxx.createIndex('xxxx', ['userId', 'email.qq']);
这种方式是错误的,不支持'email.qq'
选项对象(options)
一个可选的对象,用于定义索引的行为,跟 MySQL 中的约束条件差不错
unique
类型:boolean
描述:如果设置为 true,则索引的值必须唯一。如果尝试插入重复值,会抛出错误。默认值为 false。
multiEntry
类型:boolean
描述:如果设置为 true,并且 keyPath 指向一个数组,则数组中的每个元素都会被单独索引。默认值为 false。
事务
方法:transaction
使用:db.transaction('storeName', 'readonly')
参数:
- 仓库名(storeNames)有两种写法,字符串、字符串数组
- 字符串:表示单个对象存储的名称
- 字符串数组:表示多个对象存储的名称列表
- 事务模式(mode)
- 只读:
"readonly"
(默认值) - 读写:
"readwrite"
- 版本变更事务:
"versionchange"
- 只读:
版本变更事务:这是一种特殊的事务类型,主要用于数据库的结构修改,例如创建、删除对象存储或索引。这种事务只能在数据库版本升级时使用
一般只用只读或者读写
数据的CRUD
添加
/**
* 添加数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} data 数据
*/
function addData(db, storeName, data) {
const request = db
.transaction([storeName], "readwrite")
.objectStore(storeName)
.add(data);
// 添加成功
request.onsuccess = (event) => console.log("数据添加成功");
// 添加失败
request.onerror = (event) => console.log("数据添加失败")
}
删除
通过主键删除:
/**
* 根据主键删除数据
* @param {Object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} key 主键
*/
function delDataById(db, storeName, key) {
const request = db
.transaction([storeName], "readwrite")
.objectStore(storeName)
.delete(key);
// 删除成功
request.onsuccess = (event) => console.log("数据删除成功");
// 删除失败
request.onerror = (event) => console.log("数据删除失败");
}
通过索引或者游标来删除数据:
/**
* 通过索引和游标删除指定的数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} indexName 索引名
* @param {object} indexValue 索引值
*/
function deleteDataByIndexAndCursor(db, storeName, indexName, indexValue) {
const store = db
.transaction(storeName, "readwrite")
.objectStore(storeName);
// 通过索引和游标查询数据
const request = store
.index(indexName)
.openCursor(IDBKeyRange.only(indexValue));
request.onsuccess = function (e) {
const cursor = e.target.result;
// cursor 必须为真,否则遍历结束
if (cursor) {
// 删除当前记录
const deleteRequest = cursor.delete();
// 打印提示
deleteRequest.onsuccess = () => console.log("游标删除记录成功");
deleteRequest.onerror = () => console.log("游标删除记录失败");
// 继续遍历下一条记录
cursor.continue();
}
};
request.onerror = (e) => console.log("游标删除失败")
}
修改
/**
* 更新数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {object} data 数据
*/
function updateData(db, storeName, data) {
const request = db
.transaction([storeName], "readwrite")
.objectStore(storeName)
.put(data);
// 更新成功
request.onsuccess = () => console.log("数据更新成功");
// 更新失败
request.onerror = () => console.log("数据更新失败");
}
直接使用 put()
方法即可更新。如果数据不存在的话他就会创建一条新数据,反之则更新数据。
判断数据是否存在:在需要更新数据的时候把主键写上(就写到 data 中)
查询
主键查询
/**
* 通过主键读取数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} key 主键值
*/
function getDataByKey(db, storeName, key) {
return new Promise((resolve, reject) => {
const request = db.transaction([storeName]) // 事务
.objectStore(storeName) // 仓库对象
.get(key); // 通过主键获取单个对象
// 读取成功
request.onsuccess = (event) => resolve(request.result);
// 读取失败
request.onerror = (event) => reject(event.target.error);
});
}
不仅仅只有 get()
方法,还有:
objectStore.get()
:用于根据主键获取单个对象。参数是主键值,可以是字符串、数字等objectStore.getAll()
:用于获取所有记录或指定范围内的记录。可以不带参数获取全部,或带查询参数和数量限制。例如,getAll()
返回所有,getAll(query, count)
返回匹配查询的记录,最多 count 条;query 参数可以是IDBKeyRange
对象objectStore.getKey()
:根据主键获取对应的主键值,通常用于检查是否存在某个主键objectStore.getAllKeys()
:获取所有主键或指定范围内的主键。同样可以不带参数或带查询和数量限制
游标查询
/**
* 通过游标读取数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
*/
function getDataByCursor(db, storeName) {
const request = db
.transaction(storeName, "readwrite") // 事务
.objectStore(storeName) // 仓库对象
.openCursor() // 开启游标
// 存储容器
const list = [];
// 游标开启成功,逐行读数据
request.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) {
list.push(cursor.value); // 保存对象
cursor.continue(); // 下一条数据
} else console.log("游标读取的数据:", list);
}
// 游标开启失败
request.onerror = (e) => console.log("游标读取失败")
}
对于 openCursor()
方法的参数:
range
:IDBKeyRange 对象direction
:遍历方向"next"
:按升序遍历(从最小到最大)。"nextunique"
:按升序遍历,但跳过重复键。"prev"
:按降序遍历(从最大到最小)。"prevunique"
:按降序遍历,但跳过重复键。
索引查询
/**
* 通过索引读取数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} indexName 索引名称
* @param {string} indexValue 索引值
*/
function getDataByIndex(db, storeName, indexName, indexValue) {
const request = db
.transaction(storeName, "readwrite")
.objectStore(storeName)
.index(indexName)
.get(indexValue);
// 查询成功
request.onsuccess = (e) => console.log("索引查询结果:", e.target.result);
// 查询失败
request.onerror = () => console.log("事务失败");
}
上满 index()
后面的 get()
和主键查询中的 get()
是一样的。不仅仅能使用 get 也能使用另外的三个方法
索引+游标查询
/**
* 通过索引和游标读取数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} indexName 索引名称
* @param {string} indexValue 索引值
*/
function getDataByIndexAndCursor(db, storeName, indexName, indexValue) {
const request = db
.transaction(storeName, "readwrite")
.objectStore(storeName)
.index(indexName)
.openCursor(IDBKeyRange.only(indexValue))
// 容器
const list = [];
// 查询成功
request.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) {
list.push(cursor.value);
cursor.continue();
} else console.log("索引查询结果:", list);
}
// 查询失败
request.onerror = () => console.log("事务失败");
}
当单独使用索引查询时使用 getAll()
的话会把所有内容都出来,如果想过滤结果就可以通过游标一条一条过滤
IDBKeyRange 对象
它用于定义数据库查询时的键值范围
使用场景
- 精确匹配某个键值。
- 查询大于、小于或介于特定范围的键。
- 结合游标(cursor)遍历或筛选数据。
静态方法
共有四个静态方法:
IDBKeyRange.only(key)
:精确匹配单个键
IDBKeyRange.lowerBound(lower, [open=false])
:定义下限范围 >= lower
如果第二个参数为 true 那么就是 > lower
IDBKeyRange.upperBound(upper, [open=false])
:定义上限范围 <= upper
如果第二个参数为 true 那么就是 < upper
IDBKeyRange.bound(lower, upper, [lowerOpen=false], [upperOpen=false])
:同时定义上下限范围,后面的 open 跟上面一样
键的定义:在以上的方法中最终都是针对键来匹配的,但是对象存储(objectStore)的主键(primary key)和索引(index)的键(key)都可能使用IDBKeyRange。因此,匹配时比如使用
IDBKeyRange.only
既可以匹配主键,也可以匹配索引的键,具体取决于调用的上下文。例如,如果在对象存储上使用,就是主键;如果在索引上使用,则是该索引的键
注意事项
键类型一致性:键值类型需与对象存储或索引的键类型一致(如数字、字符串、Date),混合类型可能导致意外结果。
复合键的处理:若使用复合键(数组形式,如 [name, age]
),范围需按数组顺序逐级比较。例如:
IDBKeyRange.bound(['Alice', 20], ['Bob', 30]);
游标方向与范围:游标的遍历方向(next
或 prev
)影响数据顺序,需结合范围设置合理使用。
性能优化:在大型数据集中,精确的范围定义和索引使用能显著提升查询效率。
错误处理:非法键值(如未定义)会抛出错误,需用 try...catch
捕获:
try {
const range = IDBKeyRange.only(undefined);
} catch (e) {
console.error("无效键值");
}