一、前言
在前端开发过程中,偶尔会遇到需要存储大量数据在前端的情况。localstorage 单个域名存储大小只有5M,此时 indexedDB 便派上了用场。
indexedDB 没有"每个域名 5MB"那种硬性限制,存储大小与浏览器策略和设备总磁盘空间有关。一般来说不少于250MB,存储在电脑上的位置(C:\用户\用户名\AppData\Local\Google\Chrome\User Data\Default\IndexedDB)
可使用 StorageManager API 来查询当前源的配额使用情况和剩余空间
js
if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then(estimate => {
// 将字节转换为 GB,保留 2 位小数
const totalGB = (estimate.quota / (1024 ** 3)).toFixed(2);
const usedGB = (estimate.usage / (1024 ** 3)).toFixed(2);
const usagePercent = Math.round((estimate.usage / estimate.quota) * 100);
console.log('总配额(GB):', totalGB + ' GB');
console.log('已使用(GB):', usedGB + ' GB');
console.log('可用比例:', usagePercent + '%');
});
} else {
console.log('当前浏览器不支持 Storage Manager API');
}
提示:无痕模式下,可用空间会小很多。以我的电脑为例,使用Chrome浏览器正常模式下配额142.39 GB。使用Chrome浏览器无痕模式下配额1.59 GB。但不管怎么说,也比localstorage大不少
兼容性目前良好:

indexedDB 有较大的存储空间,这是它最大的优点。它的缺点也很明显,API比较复杂,有上手难度。如果你不在意使用细节,或者项目中可以随意添加第三方包,而不在乎项目的体积大小,可直接使用库:localforage
二、特点
-
储存空间大:比 LocalStorage 大得多
-
事务性:所有数据库操作都必须在事务中完成。这保证了数据的完整性和一致性。如果在事务中某一步操作失败,整个事务都会回滚,数据库将恢复到操作前的状态,永远不会出现"部分更新"的脏数据
-
键值对存储:数据以"对象存储"的形式存放,类似于数据库中的表。每个数据项通过一个唯一的主键来标识。值可以是几乎任何 JavaScript 类型(对象、数组、File、Blob 等)
-
异步操作:大规模的数据读写不会阻塞用户界面(UI线程)
-
同源限制:网页只能访问自身域名下的数据库
-
数据库版本管理:内置版本控制系统。当应用需要更改数据库结构时,可以通过升级版本号来触发 onupgradeneeded 事件,在此事件中执行创建或修改结构的操作。同一时刻只能有一个版本,每个域名可以建多个数据库
三、基础使用
1. 新建数据库(indexDB)
open方法接收两个参数,第一个是数据库名,第二个是数据库的版本号
在操作数据的时候,需要更新版本号才生效
js
const request = window.indexedDB.open("myDatabase", 1);
request.onsuccess = function (res) {
console.log("连接数据库成功", res);
};
request.onerror = function (error) {
console.log("连接数据库失败", error);
};
request.onupgradeneeded = function (res) {
console.log("indexedDB 升级成功", res);
};
- 新建表(对象仓库)
创建一个对象仓库必须在upgradeneeded事件中,而upgradeneeded事件只会在版本号更新的时候触发,这是因为不允许数据库中的数据仓库在同一版本中发生变化
js
//版本号变为2
const request = window.indexedDB.open("myDatabase", 2);
request.onupgradeneeded = function (res) {
const db = res.target.result;
//如果Users表不存在则创建,并插入数据
if (!db.objectStoreNames.contains("Users")) {
db.createObjectStore("Users", { keyPath: "userId", autoIncrement: false });
}
};
createObjectStore方法接受两个参数,第一个参数是对象仓库的名称,第二个参数用于指定数据的主键,主键的值必须是唯一且非空的,以及是否自增主键。

keyPath的值可以是一个数组,例如需要存储不同用户设置的标签颜色。用户的id和标签的key可共同作为主键,以满足主键值的唯一性。
db.createObjectStore("Users", { keyPath: ['userId', 'key'] });
3. 事务
IndexDB的读/写,都要通过事务操作。
特点一:事务保证数据库操作要么全部成功,要么全部失败进行回滚,恢复到上一状态。
js
// 开启事务
const transaction = db.transaction("Users", "readwrite");
第一个参数是要操作的表的名称
第二个是要创建的事务模式,如readonly只能进行读操作,readwrite能进行读写操作
特点二:它是自动提交的。即所有请求都已完成,并且事件循环再次变为空闲状态时,事务会自动提交并随之关闭。这意味着,不能在异步中调用事务
js
// ❌ 错误写法:事务会在 setTimeout 回调执行前就自动提交并关闭了!
let transaction = db.transaction(["books"], "readwrite");
let store = transaction.objectStore("books");
setTimeout(() => {
// 这里会抛出错误:Transaction is inactive
store.put({id: 1, title: "Book Title"});
}, 1000);
4. 增加数据 add
1. 增加单条数据
在数据库连接成功的onsuccess事件中,进行数据操作
js
request.onsuccess = function (res) {
const db = res.target.result;
//判断是否存在Users表
if (db.objectStoreNames.contains("Users")) {
//开启事务,允许读写操作
const transaction = db.transaction("Users", "readwrite");
//获取对象存储空间,以便后续的数据操作
const store = transaction.objectStore("Users");
//add函数传入数据,这里userId是主键
const reqAdd = store.add({ userId: 1, userName: "李白", age: 24 });
reqAdd.onsuccess = function (event) {
console.log("数据添加成功", event);
};
reqAdd.onerror = function (event) {
console.log("数据添加失败", event);
};
}
};
在开发者工具中,点击 Refresh database,可以看到数据已被添加到Users对象仓库中
2. 增加多条数据
通过监听事务的oncomplete事件,判断是否操作完成
js
function addDataToDB(dataArray, store, transaction) {
dataArray.forEach((data) => {
store.add(data);
});
return new Promise((resolve, reject) => {
transaction.oncomplete = () => {
resolve();
};
transaction.onerror = (event) => {
reject(event.target.error);
};
});
}
// 使用示例
const dataToAdd = [
{ userId: 1, name: "John", age: 20 },
{ userId: 2, name: "Jane", age: 20 },
{ userId: 3, name: "Mike", age: 20 },
];
request.onsuccess = function (res) {
const db = res.target.result;
//如果存在Users表
if (db.objectStoreNames.contains("Users")) {
//开启事务
const transaction = db.transaction("Users", "readwrite");
//获取对象存储空间,进行数据操作
const store = transaction.objectStore("Users");
addDataToDB(dataToAdd, store, transaction)
.then(() => {
console.log("数据添加成功");
})
.catch((error) => {
console.error("数据添加失败", error);
});
}
};
5. 读取数据 get
- 获取单条数据
js
request.onsuccess = function (res) {
const db = res.target.result;
if (db.objectStoreNames.contains("Users")) {
//开启事务,只允许读操作
const transaction = db.transaction("Users", "readonly");
//获取对象存储空间,进行数据操作
const store = transaction.objectStore("Users");
//传入主键值
const reqGet = store.get(1);
reqGet.onsuccess = function (event) {
if (event.target.result) {
console.log("数据获取成功", event.target.result);
} else {
console.log("未获取到数据");
}
};
reqGet.onerror = function (event) {
console.log("数据获取失败", event);
};
}
};
如果主键是由多个值组成,例如创建表时,keyPath是个数组
js
db.createObjectStore("Users", { keyPath: \['userId', 'key'] });
那么取值时,也通过数组取值
js
//userId与key是变量,根据实际情况传入具体的值
const reqGet = store.get(\[userId, key]);
- 获取所有数据
js
const reqGet = store.getAll();
6. 更新数据 put
使用 put 进行数据操作时,"无则增,有则改"。如果主键不存在,则新建数据。如果主键存在,则更新数据。除非明确数据只能新增,不能更新,其余情况下,put 可以代替add。
js
request.onsuccess = function (res) {
const db = res.target.result;
if (db.objectStoreNames.contains("Users")) {
//开启事务,允许读写操作
const transaction = db.transaction("Users", "readwrite");
//获取对象存储空间,进行数据操作
const store = transaction.objectStore("Users");
//put方法根据主键值更新数据
const reqPut = store.put({ userId: 1, userName: "张三", age: 20 });
reqPut.onsuccess = function (event) {
console.log("数据更新成功", event);
};
reqPut.onerror = function (event) {
console.log("数据更新失败", event);
};
}
};
7. 删除数据 delete
js
request.onsuccess = function (res) {
const db = res.target.result;
if (db.objectStoreNames.contains("Users")) {
//开启事务,允许读写操作
const transaction = db.transaction("Users", "readwrite");
//获取对象存储空间,进行数据操作
const store = transaction.objectStore("Users");
//delete方法根据主键值删除数据
const reqDelete = store.delete(1);
reqDelete.onsuccess = function (event) {
console.log("数据删除成功", event);
};
reqDelete.onerror = function (event) {
console.log("数据删除失败", event);
};
}
};
四、进阶
1. IDBKeyRange
IDBKeyRange:用于定义和限制在数据库查询中使用的键的范围,从而根据条件查询多条数据
1. 精确匹配
查找主键值为1的数据
js
const range = IDBKeyRange.only("1");
2. 范围匹配
查找主键值为1到10的数据
如果第三个参数为true,则表示不包含最小键值1,如果第四参数为true,则表示不包含最大键值10,默认都为false
js
const range = IDBKeyRange.bound(1, 10, false, false);
3. 上下界匹配
(1)下界匹配
第二个参数可选,为true则表示不包含最小主键1,false则包含,默认为false
js
const range = IDBKeyRange.lowerBound(1, false);
(2)上界匹配
第二个参数可选,为true则表示不包含最大主键10,false则包含,默认为false
js
const range = IDBKeyRange.upperBound(10, false);
当主键由一个字段组成,并且根据主键查询时,可使用 store.getAll+ IDBKeyRange 直接查询数据。
示例:获取所有主键值小于等于10的数据
js
request.onsuccess = function (res) {
const db = res.target.result;
//如果存在Users表
if (db.objectStoreNames.contains("Users")) {
//开启事务
const transaction = db.transaction("Users", "readonly");
//获取对象存储空间,进行数据操作
const store = transaction.objectStore("Users");
//获取所有主键值小于等于10的数据
const reqGet = store.getAll(IDBKeyRange.upperBound(10));
reqGet.onsuccess = function (event) {
if (event.target.result) {
console.log("数据获取成功", event.target.result);
} else {
console.log("未获取到数据");
}
};
reqGet.onerror = function (event) {
console.log("数据获取失败", event);
};
}
};
2. 游标
作用:选定范围,并遍历和操作数据
js
openCursor(range, direction);
range:表示范围,IDBKeyRange的值
direction:表示方向,next 升序(默认);nextunique 升序,索引值相等则只读第一条;prev 降序;prevunique 降序,索引值相等则只读第一条;
js
request.onsuccess = function (res) {
const db = res.target.result;
//如果存在Users表
if (db.objectStoreNames.contains("Users")) {
//开启事务
const transaction = db.transaction("Users", "readwrite");
//获取对象存储空间,进行数据操作
const store = transaction.objectStore("Users");
//主键1到10
const range = IDBKeyRange.bound(1, 10);
const request = store.openCursor(range);
request.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.value); //拿到数据
// cursor.updata(value) 更新数据
// cursor.delete() 删除数据
// cursor.advance(count) 向前跳过指定数量的记录,可用作分页
cursor.continue();//继续读取下一条
} else {
console.log("没有更多result");
}
};
request.onerror = function (event) {
console.log("数据查找失败", event);
};
}
};
3. 索引
索引极大地扩展了数据库的查询能力,上述示例是基于主键查询。如果一个数据结构为{ userId: 1, userName: "张三", age: 20 },其中userId为主键,那么要查询所有age小于18的数据时,就需要借助索引。
js
createIndex(name, keyPath, optionalParameters)
name:索引名称
keyPath:要索引的对象属性,可以是单个key值,也可以是key值组成的数组
optionalParameters:可选参数{unique, multiEntry}
unique:指定被索引的属性值是否可以重复,为true代表不能重复,为false时可以重复。默认false
multiEntry:当第二个参数keyPath为数组时,如果multiEntry是true,则会以数组中的每个元素建立一条索引。如果是false,将整个数组作为一个单一的复合键存入索引。默认为false。
1. 创建索引
在数据库版本升级时(在 onupgradeneeded 事件中)创建
js
request.onupgradeneeded = function (res) {
const db = res.target.result;
//如果Users表不存在则创建,并插入数据
if (!db.objectStoreNames.contains("Users")) {
const store = db.createObjectStore("Users", {
keyPath: "userId",
autoIncrement: false,
});
//建立索引,索引名ageIndex,与原age属性建立索引,索引值可以重复
store.createIndex("ageIndex", "age", { unique: false });
}
};
2. 获取单个值 get
获取单个值的前提,创建索引时unique为true,否则查询到的结果一直为undefined
js
//确保存储的age值唯一
store.createIndex("ageIndex", "age", { unique: true });
查询值
js
const transaction = db.transaction("Users", "readonly");
const store = transaction.objectStore("Users");
const index = store.index("ageIndex");
const request = index.get(20);
request.onsuccess = function (event) {
let user = event.target.result;
console.log("Found user:", user);
};
3. 获取所有值 getAll
不管值是否唯一,获取所有值
js
const request = index.getAll(20);
4. 通过索引打开游标
可对范围内的数据进行读取或修改
js
request.onsuccess = function (res) {
const db = res.target.result;
//如果存在Users表
if (db.objectStoreNames.contains("Users")) {
//开启事务
const transaction = db.transaction("Users", "readonly");
//获取对象存储空间,进行数据操作
const store = transaction.objectStore("Users");
//获取索引
const index = store.index("ageIndex");
//使用索引查询年龄最低为20的数据
const request = index.openCursor(IDBKeyRange.lowerBound(20));
request.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.value); //拿到数据
cursor.continue(); //继续读取下一条
} else {
console.log("没有更多result");
}
};
request.onerror = function (event) {
console.log("数据查找失败", event);
};
}
};
- 封装示例
为了更方便的使用,可以将IndexDB的操作封装成一个类
js
/**
* 通用 IndexedDB 单表 CRUD 封装类
* 支持自定义对象结构
*/
export interface IndexedDBOptions {
dbName: string;
version?: number;
storeName: string;
keyPath: string;
}
export class IndexedDBManager<T extends Record<string, any>> {
private db: IDBDatabase | null = null;
private isInitialized = false;
private options: IndexedDBOptions;
constructor(options: IndexedDBOptions) {
this.options = {
version: 1,
...options,
};
}
async init(): Promise<void> {
if (this.isInitialized) return;
const { dbName, version, storeName, keyPath } = this.options;
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onerror = () => {
console.error("Failed to open IndexedDB:", request.error);
reject(request.error);
};
request.onsuccess = () => {
this.db = request.result;
this.isInitialized = true;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName, { keyPath });
}
};
});
}
async get(key: IDBValidKey): Promise<T | undefined> {
await this.init();
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error("Database not initialized"));
return;
}
const transaction = this.db.transaction([this.options.storeName], "readonly");
const store = transaction.objectStore(this.options.storeName);
const request = store.get(key);
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve(request.result as T | undefined);
};
});
}
async getAll(): Promise<T[]> {
await this.init();
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error("Database not initialized"));
return;
}
const transaction = this.db.transaction([this.options.storeName], "readonly");
const store = transaction.objectStore(this.options.storeName);
const request = store.getAll();
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve(request.result as T[]);
};
});
}
async put(data: T): Promise<void> {
await this.init();
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error("Database not initialized"));
return;
}
const transaction = this.db.transaction([this.options.storeName], "readwrite");
const store = transaction.objectStore(this.options.storeName);
const request = store.put(data);
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve();
};
});
}
async delete(key: IDBValidKey): Promise<void> {
await this.init();
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error("Database not initialized"));
return;
}
const transaction = this.db.transaction([this.options.storeName], "readwrite");
const store = transaction.objectStore(this.options.storeName);
const request = store.delete(key);
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve();
};
});
}
async clear(): Promise<void> {
await this.init();
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error("Database not initialized"));
return;
}
const transaction = this.db.transaction([this.options.storeName], "readwrite");
const store = transaction.objectStore(this.options.storeName);
const request = store.clear();
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve();
};
});
}
}