IndexedDB 使用指南
前言
在上一篇文章介绍了前端跨页面通讯终极指南⑧:Cookie 用法全解析,下一篇是介绍前端跨页面通讯终极指南⑨:# IndexedDB 用法全解析,考虑到这种方式并不是常用,先介绍下IndexedDB的使用方法,再对跨页面通信进行总结。
下面介绍下IndexedDB概念以及常见用法。
1. 基本概念
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。
IndexedDB是浏览器提供的 NoSQL 数据库,用于在客户端存储大量结构化数据。它支持事务、索引查询和异步操作,适合离线应用、数据缓存等场景。
数据库层级结构
Database (数据库)
├── Object Store (对象存储空间)
│ ├── Index (索引)
│ └── Data Records (数据记录)
└── Object Store (对象存储空间)
关键特性
- 异步操作:所有操作都是异步的,使用事件或Promise
- 事务支持:原子性操作,要么全部成功要么全部回滚
- 索引查询:支持基于索引的高效查询
- 存储限制:通常限制为几百MB(取决于浏览器)
- 持久化存储:数据在浏览器关闭后仍然保留
2. 核心API
2.1 打开数据库
const request = indexedDB.open('dbName', version);
2.2 创建/升级数据库
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建或升级存储空间
};
2.3 事务操作
const transaction = db.transaction(storeNames, mode);
const store = transaction.objectStore(storeName);
这里的事务需要特别说明下。为什么要引入事务?
我们用银行转账的案例进行说明:
假设你要从 A账户 转账 100 元给 B账户。这个过程包含两个必不可少的步骤:
- 从 A 账户扣除 100 元。
- 给 B 账户增加 100 元。
如果没有事务(不安全的情况): 如果在步骤 1 完成后,突然停电了或系统出错了,步骤 2 没执行。结果就是:A 的钱少了,B 没收到钱,这 100 元凭空消失了。
有了事务(安全的情况): 当你点击“确认转账”时,数据库开启了一个“事务”:
- 原子性:数据库将步骤 1 和步骤 2 打包成一个整体。只有当两个步骤都成功时,修改才会生效;只要中间任意一步出错,数据库会自动“回滚”,让一切回到转账前的样子(A 的钱没少,B 的钱没多)。
对应到 IndexedDB
-
银行 -> 整个 IndexedDB 数据库 (
db) -
A账户 和 B账户 -> 相同的对象存储 (
accounts) -
转账这个行为 -> 一个事务 (
transaction)
现在我们来看代码如何实现这个逻辑:
// 1. 开启事务
// 参数 ['accounts'] 指定了事务的【作用域】:只允许操作 'accounts' 这个表
// 参数 'readwrite' 指定了【模式】:允许修改数据(读写)
const transaction = db.transaction(['accounts'], 'readwrite');
// 2. 获取对象仓库(表)
const store = transaction.objectStore('accounts');
// --- 以下是事务内的具体操作 ---
// 操作 A:更新账户 A 的余额(假设原为 1000,现改为 900)
store.put({ id: 'A', balance: 900 });
// 操作 B:更新账户 B 的余额(假设原为 1000,现改为 1100)
store.put({ id: 'B', balance: 1100 });
// --- 监听事务结果 ---
// 如果上面所有操作都成功
transaction.oncomplete = function(event) {
console.log("转账成功!数据已永久保存。");
};
// 如果中间任何一步报错(比如账户 B 不存在,或者磁盘满了)
transaction.onerror = function(event) {
console.log("转账失败!刚才的修改全部撤销,A 的余额变回 1000。");
};
通过这个例子我们可以清楚的看到IndexedDB事务的核心作用:
- 原子性:将单个或多个相关的数据库操作(扣款、存款)打包成一个不可分割的整体。
- 一致性:确保数据库从一个正确的状态(转账前)转换到另一个正确的状态(转账后)。如果中途失败,则会回到初始状态,不会出现数据不一致(钱少了)的中间状态。
2.4. 常用操作方法
// 添加数据
store.add(data);
// 更新数据
store.put(data);
// 删除数据
store.delete(key);
// 获取数据
store.get(key);
// 获取所有数据
store.getAll();
// 使用索引查询
store.index('indexName').get(value);
3. 数据库操作
3.1 打开数据库
const openDB = (dbName, version) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 数据库升级逻辑
};
});
};
3.2 关闭数据库
db.close();
3.3 删除数据库
const deleteDB = (dbName) => {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(dbName);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve('数据库删除成功');
});
};
4. 事务处理
4.1 创建事务
const transaction = db.transaction(['store1', 'store2'], 'readwrite');
4.2 事务模式
-
'readonly':只读事务 -
'readwrite':读写事务 -
'versionchange':版本变更事务
4.3 事务错误处理
transaction.onerror = (event) => {
console.error('事务错误:', event.target.error);
};
transaction.onabort = () => {
console.log('事务已回滚');
};
transaction.oncomplete = () => {
console.log('事务完成');
};
4.4 手动回滚事务
transaction.abort();
5. 数据存储
5.1 创建对象存储空间
const store = db.createObjectStore('users', {
keyPath: 'id', // 使用数据中的id字段作为主键
autoIncrement: true // 自动生成主键
});
5.2 添加数据
const addData = (store, data) => {
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
5.3 更新数据
const updateData = (store, data) => {
return new Promise((resolve, reject) => {
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
5.4 删除数据
const deleteData = (store, key) => {
return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
};
6. 查询操作
6.1 获取单个数据
const getData = (store, key) => {
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
6.2 获取所有数据
const getAllData = (store) => {
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
6.3 范围查询
const rangeQuery = (store, range) => {
return new Promise((resolve, reject) => {
const request = store.openCursor(range);
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
};
7. 索引使用
7.1 创建索引
store.createIndex('nameIndex', 'name', { unique: false });
store.createIndex('emailIndex', 'email', { unique: true });
7.2 使用索引查询
const queryByIndex = (store, indexName, value) => {
return new Promise((resolve, reject) => {
const index = store.index(indexName);
const request = index.get(value);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
7.3 范围索引查询
const rangeQueryByIndex = (store, indexName, range) => {
return new Promise((resolve, reject) => {
const index = store.index(indexName);
const request = index.openCursor(range);
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
};
8. 完整示例
用户管理系统
// 用户管理系统
class UserManager {
constructor(dbName = 'UserDB', version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async init() {
this.db = await this.openDB();
return this;
}
async openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
store.createIndex('email', 'email', { unique: true });
store.createIndex('createdAt', 'createdAt', { unique: false });
}
};
});
}
async addUser(user) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
user.createdAt = new Date().toISOString();
const request = store.add(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getUserById(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getUserByEmail(email) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('email');
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async updateUser(id, updates) {
const user = await this.getUserById(id);
if (!user) throw new Error('用户不存在');
const updatedUser = { ...user, ...updates };
return this.putUser(updatedUser);
}
async putUser(user) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.put(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async deleteUser(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async getAllUsers() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async searchUsers(query) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('name');
const range = IDBKeyRange.bound(query, query + '\uffff');
const request = index.openCursor(range);
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
}
// 使用示例
async function demo() {
const userManager = new UserManager();
await userManager.init();
// 添加用户
await userManager.addUser({
id: 'user1',
name: '张三',
email: 'zhangsan@example.com',
age: 25
});
// 获取用户
const user = await userManager.getUserById('user1');
console.log('获取用户:', user);
// 更新用户
await userManager.updateUser('user1', { age: 26 });
// 搜索用户
const users = await userManager.searchUsers('张');
console.log('搜索结果:', users);
// 删除用户
await userManager.deleteUser('user1');
}
![]()
总结
最后总结一下:IndexedDB是前端的数据库,通常在 Web Storage 无法满足容量要求的场景下才使用,它能够存储大量数据,一般不轻易用。