目录
- [IndexedDB 简介](#IndexedDB 简介)
- 核心概念与API概览
- 环境准备与兼容性
- 基础操作:打开数据库
- [对象仓库(Object Store)操作](#对象仓库(Object Store)操作)
- [CRUD 完整示例](#CRUD 完整示例)
- 索引与查询
- 游标与分页
- 事务管理
- 实战案例:离线笔记应用
- 性能优化与最佳实践
- 常见问题与调试技巧
1. IndexedDB 简介
1.1 什么是 IndexedDB?
IndexedDB 是浏览器内置的非关系型数据库(NoSQL),用于在客户端存储大量结构化数据。它提供以下特性:
- 容量大:通常可达 250MB 以上,远超 localStorage(5-10MB)
- 异步操作:不阻塞主线程,基于事件或 Promise
- 支持索引:可以建立索引实现高效查询
- 事务支持:保证数据一致性(ACID 特性)
- 存储类型丰富:可以存储任意 JavaScript 对象、二进制数据(Blob、ArrayBuffer)
1.2 适用场景
| 场景 | 说明 |
|---|---|
| 离线应用(PWA) | 缓存 API 数据,实现离线访问 |
| 大数据量存储 | 超过 localStorage 容量限制的场景 |
| 富客户端应用 | 在线文档编辑器、邮件客户端等 |
| 缓存层 | 作为远程数据的本地缓存,提升加载速度 |
| 用户生成内容 | 本地草稿、用户配置等 |
2. 核心概念与API概览
2.1 核心概念
数据库(Database)
└── 对象仓库(Object Store) // 类似关系型数据库的"表"
└── 索引(Index) // 加速查询
└── 记录(Record) // 键值对数据
| 概念 | 说明 |
|---|---|
| 数据库 | 每个域名可创建多个数据库,每个数据库有独立的版本号 |
| 对象仓库 | 存储数据的容器,类似"表"。每个对象仓库有一个主键(key) |
| 索引 | 基于对象属性的查询加速器,类似 SQL 的索引 |
| 事务 | 对数据库的一组操作,保证原子性 |
| 游标 | 遍历数据的指针,用于范围查询和分页 |
2.2 主要 API
javascript
// 核心对象
window.indexedDB // 全局入口
IDBRequest // 所有异步操作返回的请求对象
IDBDatabase // 数据库实例
IDBObjectStore // 对象仓库
IDBIndex // 索引
IDBCursor // 游标
IDBTransaction // 事务
3. 环境准备与兼容性
3.1 浏览器兼容性
IndexedDB 支持所有现代浏览器:
- Chrome/Edge 24+
- Firefox 16+
- Safari 10+(iOS 10+)
- Opera 15+
3.2 检测兼容性
javascript
function checkIndexedDBSupport() {
if (!window.indexedDB) {
console.error('当前浏览器不支持 IndexedDB');
return false;
}
return true;
}
// 使用示例
if (checkIndexedDBSupport()) {
console.log('IndexedDB 可用');
}
3.3 辅助工具
- Chrome DevTools:Application → IndexedDB,可查看和编辑数据
- Firefox DevTools:Storage → IndexedDB
- 封装库推荐 :
idb(轻量级 Promise 封装)Dexie.js(功能完善的封装)
本教程使用原生 API,便于理解底层原理。
4. 基础操作:打开数据库
4.1 打开/创建数据库
javascript
// 打开数据库(如果不存在则创建)
// 参数:数据库名称,版本号
const request = indexedDB.open('MyDatabase', 1);
// 成功回调
request.onsuccess = (event) => {
const db = event.target.result;
console.log('数据库打开成功', db);
};
// 失败回调
request.onerror = (event) => {
console.error('数据库打开失败', event.target.error);
};
// 版本更新回调(首次创建或版本号升级时触发)
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
const newVersion = event.newVersion;
console.log(`数据库版本升级: ${oldVersion} → ${newVersion}`);
// 在此处创建对象仓库和索引
};
4.2 数据库版本管理
IndexedDB 使用版本号来管理数据库结构变化(创建/删除对象仓库、索引等)。
javascript
// 版本升级时创建对象仓库
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 检查对象仓库是否已存在,避免重复创建
if (!db.objectStoreNames.contains('users')) {
// 创建对象仓库,指定主键
const store = db.createObjectStore('users', {
keyPath: 'id', // 使用对象的 id 属性作为主键
autoIncrement: false // 是否自动递增(设为 true 时自动生成主键)
});
console.log('users 表创建成功');
}
// 如果需要删除对象仓库(谨慎使用)
// if (db.objectStoreNames.contains('oldStore')) {
// db.deleteObjectStore('oldStore');
// }
};
4.3 封装数据库连接
javascript
class DatabaseManager {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async connect() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
this.db = event.target.result;
this.onUpgrade(this.db);
resolve(this.db);
};
});
}
onUpgrade(db) {
// 子类可覆盖此方法
console.log('数据库升级回调');
}
getDB() {
if (!this.db) {
throw new Error('请先调用 connect() 方法');
}
return this.db;
}
}
5. 对象仓库(Object Store)操作
5.1 主键的定义方式
javascript
// 方式1:使用 keyPath(对象属性作为主键)
const store1 = db.createObjectStore('users', { keyPath: 'id' });
// 添加数据:需要对象包含 id 属性
// 方式2:使用 autoIncrement(自动生成递增主键)
const store2 = db.createObjectStore('logs', { autoIncrement: true });
// 添加数据时自动生成主键 1, 2, 3...
// 方式3:不指定 keyPath,手动设置主键
const store3 = db.createObjectStore('config');
// 添加时通过 add(data, key) 的第二个参数指定主键
5.2 创建带索引的对象仓库
javascript
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建 users 表
const userStore = db.createObjectStore('users', { keyPath: 'id' });
// 创建索引(加速查询)
// 参数:索引名称,索引属性,配置项
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('age', 'age', { unique: false });
userStore.createIndex('name_age', ['name', 'age']); // 复合索引
// 创建 posts 表
const postStore = db.createObjectStore('posts', { keyPath: 'id', autoIncrement: true });
postStore.createIndex('userId', 'userId', { unique: false });
postStore.createIndex('createdAt', 'createdAt', { unique: false });
};
5.3 索引的配置选项
| 选项 | 类型 | 说明 |
|---|---|---|
unique |
boolean | 索引值是否唯一(默认 false) |
multiEntry |
boolean | 当属性为数组时,是否对数组每个元素建立索引(默认 false) |
6. CRUD 完整示例
6.1 添加数据(Create)
javascript
// 添加单条数据
function addUser(db, user) {
return new Promise((resolve, reject) => {
// 创建事务(指定对象仓库和操作模式)
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
// 添加数据
const request = store.add(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
// 事务完成回调
transaction.oncomplete = () => console.log('事务完成');
transaction.onerror = () => reject(transaction.error);
});
}
// 使用示例
const newUser = {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
age: 25,
createdAt: new Date()
};
addUser(db, newUser).then(() => {
console.log('用户添加成功');
}).catch(err => {
console.error('添加失败', err);
});
6.2 读取数据(Read)
javascript
// 根据主键读取单条数据
function getUserById(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 使用示例
getUserById(db, 1).then(user => {
if (user) {
console.log('找到用户:', user);
} else {
console.log('用户不存在');
}
});
6.3 更新数据(Update)
javascript
// 方式1:使用 put(存在则更新,不存在则新增)
function updateUser(db, user) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.put(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 方式2:先读取再修改(适用于部分更新)
async function partialUpdateUser(db, id, updates) {
const user = await getUserById(db, id);
if (!user) throw new Error('用户不存在');
const updatedUser = { ...user, ...updates };
return updateUser(db, updatedUser);
}
// 使用示例
await partialUpdateUser(db, 1, { age: 26, name: '张三丰' });
6.4 删除数据(Delete)
javascript
// 根据主键删除
function deleteUserById(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// 清空整个对象仓库
function clearStore(db, storeName) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
6.5 完整 CRUD 封装
javascript
class IndexedDBHelper {
constructor(dbName, version, upgradeCallback) {
this.dbName = dbName;
this.version = version;
this.upgradeCallback = upgradeCallback;
this.db = null;
}
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
this.db = event.target.result;
if (this.upgradeCallback) {
this.upgradeCallback(this.db);
}
resolve(this.db);
};
});
}
async add(storeName, data) {
return this._execute(storeName, 'readwrite', (store) => store.add(data));
}
async get(storeName, key) {
return this._execute(storeName, 'readonly', (store) => store.get(key));
}
async put(storeName, data) {
return this._execute(storeName, 'readwrite', (store) => store.put(data));
}
async delete(storeName, key) {
return this._execute(storeName, 'readwrite', (store) => store.delete(key));
}
async getAll(storeName) {
return this._execute(storeName, 'readonly', (store) => store.getAll());
}
_execute(storeName, mode, callback) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], mode);
const store = transaction.objectStore(storeName);
const request = callback(store);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
transaction.onerror = () => reject(transaction.error);
});
}
}
7. 索引与查询
7.1 通过索引查询单条数据
javascript
// 通过 email 索引查询用户
function getUserByEmail(db, email) {
return new Promise((resolve, reject) => {
const transaction = 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);
});
}
// 使用示例
const user = await getUserByEmail(db, 'zhangsan@example.com');
7.2 通过索引查询多条数据
javascript
// 查询所有年龄为 25 的用户
function getUsersByAge(db, age) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('age');
const request = index.getAll(age);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
7.3 范围查询(使用 IDBKeyRange)
javascript
// IDBKeyRange 常用方法
// 范围查询:age >= 18
const range1 = IDBKeyRange.lowerBound(18);
// 范围查询:age < 30
const range2 = IDBKeyRange.upperBound(30, true); // true 表示不包含上界
// 范围查询:age >= 18 且 age <= 30
const range3 = IDBKeyRange.bound(18, 30);
// 范围查询:age = 25
const range4 = IDBKeyRange.only(25);
// 使用示例:查询年龄在 20-30 之间的用户
function getUsersByAgeRange(db, minAge, maxAge) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('age');
const range = IDBKeyRange.bound(minAge, maxAge);
const request = index.getAll(range);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
7.4 复合索引查询
javascript
// 创建复合索引(name + age)
// 在 onupgradeneeded 中创建
userStore.createIndex('name_age', ['name', 'age']);
// 查询 name = '张三' 且 age = 25 的用户
function getUserByNameAndAge(db, name, age) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('name_age');
const request = index.get([name, age]);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
8. 游标与分页
8.1 使用游标遍历数据
javascript
// 遍历所有用户
function getAllUsersWithCursor(db) {
return new Promise((resolve, reject) => {
const users = [];
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const cursorRequest = store.openCursor();
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
users.push(cursor.value);
cursor.continue(); // 移动到下一条
} else {
resolve(users); // 遍历完成
}
};
cursorRequest.onerror = () => reject(cursorRequest.error);
});
}
8.2 带条件的游标查询
javascript
// 查询年龄 >= 18 的用户,并限制返回数量
function getUsersWithCondition(db, minAge, limit = 10) {
return new Promise((resolve, reject) => {
const users = [];
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('age');
const range = IDBKeyRange.lowerBound(minAge);
const cursorRequest = index.openCursor(range);
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && users.length < limit) {
users.push(cursor.value);
cursor.continue();
} else {
resolve(users);
}
};
cursorRequest.onerror = () => reject(cursorRequest.error);
});
}
8.3 分页实现
javascript
// 分页查询
async function getUsersPaginated(db, pageSize = 10, pageIndex = 0) {
return new Promise((resolve, reject) => {
const users = [];
let skipCount = pageSize * pageIndex;
let currentIndex = 0;
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const cursorRequest = store.openCursor();
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
resolve(users);
return;
}
if (currentIndex >= skipCount && users.length < pageSize) {
users.push(cursor.value);
}
currentIndex++;
if (users.length < pageSize) {
cursor.continue();
} else {
resolve(users);
}
};
cursorRequest.onerror = () => reject(cursorRequest.error);
});
}
// 使用示例
const page1 = await getUsersPaginated(db, 10, 0); // 第一页
const page2 = await getUsersPaginated(db, 10, 1); // 第二页
8.4 反向遍历(倒序)
javascript
// 按创建时间倒序获取最新数据
function getLatestPosts(db, limit = 10) {
return new Promise((resolve, reject) => {
const posts = [];
const transaction = db.transaction(['posts'], 'readonly');
const store = transaction.objectStore('posts');
const index = store.index('createdAt');
// 打开反向游标
const cursorRequest = index.openCursor(null, 'prev');
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && posts.length < limit) {
posts.push(cursor.value);
cursor.continue();
} else {
resolve(posts);
}
};
cursorRequest.onerror = () => reject(cursorRequest.error);
});
}
9. 事务管理
9.1 事务的类型
| 模式 | 说明 |
|---|---|
readonly |
只读,可并发执行,性能较好 |
readwrite |
读写,独占操作,保证数据一致性 |
9.2 多对象仓库事务
javascript
// 在一个事务中操作多个对象仓库
async function transferPost(db, userId, postId, newUserId) {
return new Promise((resolve, reject) => {
// 事务包含两个对象仓库
const transaction = db.transaction(['users', 'posts'], 'readwrite');
const userStore = transaction.objectStore('users');
const postStore = transaction.objectStore('posts');
let post = null;
let newUser = null;
// 步骤1:获取帖子
const getPostRequest = postStore.get(postId);
getPostRequest.onsuccess = () => {
post = getPostRequest.result;
if (!post) {
transaction.abort();
reject(new Error('帖子不存在'));
return;
}
// 步骤2:获取目标用户
const getUserRequest = userStore.get(newUserId);
getUserRequest.onsuccess = () => {
newUser = getUserRequest.result;
if (!newUser) {
transaction.abort();
reject(new Error('目标用户不存在'));
return;
}
// 步骤3:更新帖子所属用户
post.userId = newUserId;
const updateRequest = postStore.put(post);
updateRequest.onsuccess = () => {
console.log('帖子转移成功');
};
};
};
transaction.oncomplete = () => resolve({ post, newUser });
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(new Error('事务已中止'));
});
}
9.3 事务的生命周期
javascript
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
// 事务在以下情况自动提交:
// 1. 所有请求完成且没有新请求加入
// 2. 事件循环返回
// 手动中止事务
transaction.abort();
// 监听事务状态
transaction.oncomplete = () => console.log('事务提交成功');
transaction.onerror = () => console.log('事务失败');
transaction.onabort = () => console.log('事务被中止');
10. 实战案例:离线笔记应用
10.1 数据库设计
javascript
// 数据库初始化
class NoteDatabase {
constructor() {
this.dbName = 'NoteApp';
this.version = 1;
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建 notes 表
if (!db.objectStoreNames.contains('notes')) {
const noteStore = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true
});
// 创建索引
noteStore.createIndex('title', 'title');
noteStore.createIndex('category', 'category');
noteStore.createIndex('updatedAt', 'updatedAt');
noteStore.createIndex('isDeleted', 'isDeleted');
noteStore.createIndex('category_updated', ['category', 'updatedAt']);
}
// 创建 categories 表
if (!db.objectStoreNames.contains('categories')) {
const categoryStore = db.createObjectStore('categories', {
keyPath: 'id',
autoIncrement: true
});
categoryStore.createIndex('name', 'name', { unique: true });
}
// 初始化默认分类
const categoryStore = transaction.objectStore('categories');
const defaultCategories = ['工作', '学习', '生活', '灵感'];
defaultCategories.forEach(name => {
categoryStore.add({ name });
});
};
});
}
}
10.2 笔记 CRUD 操作
javascript
class NoteService {
constructor(db) {
this.db = db;
}
// 创建笔记
async createNote(note) {
const transaction = this.db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
const newNote = {
...note,
createdAt: new Date(),
updatedAt: new Date(),
isDeleted: false
};
return new Promise((resolve, reject) => {
const request = store.add(newNote);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 获取所有笔记(排除已删除)
async getAllNotes() {
const transaction = this.db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const index = store.index('isDeleted');
const range = IDBKeyRange.only(false);
return new Promise((resolve, reject) => {
const request = index.getAll(range);
request.onsuccess = () => {
// 按更新时间倒序
const notes = request.result.sort((a, b) =>
new Date(b.updatedAt) - new Date(a.updatedAt)
);
resolve(notes);
};
request.onerror = () => reject(request.error);
});
}
// 根据分类获取笔记
async getNotesByCategory(category) {
const transaction = this.db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const index = store.index('category_updated');
const range = IDBKeyRange.bound([category, new Date(0)], [category, new Date()]);
return new Promise((resolve, reject) => {
const request = index.getAll(range);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 搜索笔记(标题或内容)
async searchNotes(keyword) {
const allNotes = await this.getAllNotes();
const lowerKeyword = keyword.toLowerCase();
return allNotes.filter(note =>
note.title?.toLowerCase().includes(lowerKeyword) ||
note.content?.toLowerCase().includes(lowerKeyword)
);
}
// 更新笔记
async updateNote(id, updates) {
const transaction = this.db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
return new Promise(async (resolve, reject) => {
// 先获取原笔记
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const oldNote = getRequest.result;
if (!oldNote) {
reject(new Error('笔记不存在'));
return;
}
const updatedNote = {
...oldNote,
...updates,
updatedAt: new Date()
};
const putRequest = store.put(updatedNote);
putRequest.onsuccess = () => resolve(updatedNote);
putRequest.onerror = () => reject(putRequest.error);
};
getRequest.onerror = () => reject(getRequest.error);
});
}
// 软删除笔记
async deleteNote(id) {
return this.updateNote(id, { isDeleted: true });
}
// 永久删除笔记
async permanentlyDeleteNote(id) {
const transaction = this.db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
return new Promise((resolve, reject) => {
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// 获取回收站笔记
async getDeletedNotes() {
const transaction = this.db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const index = store.index('isDeleted');
const range = IDBKeyRange.only(true);
return new Promise((resolve, reject) => {
const request = index.getAll(range);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 清空回收站
async clearTrash() {
const deletedNotes = await this.getDeletedNotes();
const promises = deletedNotes.map(note => this.permanentlyDeleteNote(note.id));
await Promise.all(promises);
}
}
10.3 分类管理
javascript
class CategoryService {
constructor(db) {
this.db = db;
}
// 获取所有分类
async getAllCategories() {
const transaction = this.db.transaction(['categories'], 'readonly');
const store = transaction.objectStore('categories');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 添加分类
async addCategory(name) {
const transaction = this.db.transaction(['categories'], 'readwrite');
const store = transaction.objectStore('categories');
return new Promise((resolve, reject) => {
const request = store.add({ name });
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 删除分类(同时将相关笔记移至默认分类)
async deleteCategory(id) {
const transaction = this.db.transaction(['categories', 'notes'], 'readwrite');
const categoryStore = transaction.objectStore('categories');
const noteStore = transaction.objectStore('notes');
return new Promise(async (resolve, reject) => {
// 获取要删除的分类名称
const getRequest = categoryStore.get(id);
getRequest.onsuccess = async () => {
const category = getRequest.result;
if (!category) {
reject(new Error('分类不存在'));
return;
}
// 将该分类下的笔记移到默认分类
const notesIndex = noteStore.index('category');
const range = IDBKeyRange.only(category.name);
const cursorRequest = notesIndex.openCursor(range);
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const note = cursor.value;
note.category = '未分类';
noteStore.put(note);
cursor.continue();
} else {
// 删除分类
const deleteRequest = categoryStore.delete(id);
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = () => reject(deleteRequest.error);
}
};
cursorRequest.onerror = () => reject(cursorRequest.error);
};
getRequest.onerror = () => reject(getRequest.error);
});
}
}
10.4 React/Vue 集成示例
javascript
// React Hook 示例
import { useState, useEffect, useCallback } from 'react';
function useNotes() {
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState(true);
const [noteService, setNoteService] = useState(null);
useEffect(() => {
initDB();
}, []);
async function initDB() {
const dbManager = new NoteDatabase();
await dbManager.init();
setNoteService(new NoteService(dbManager.db));
loadNotes();
}
async function loadNotes() {
if (!noteService) return;
setLoading(true);
const allNotes = await noteService.getAllNotes();
setNotes(allNotes);
setLoading(false);
}
const createNote = useCallback(async (noteData) => {
if (!noteService) return;
const newId = await noteService.createNote(noteData);
await loadNotes();
return newId;
}, [noteService]);
const updateNote = useCallback(async (id, updates) => {
if (!noteService) return;
await noteService.updateNote(id, updates);
await loadNotes();
}, [noteService]);
const deleteNote = useCallback(async (id) => {
if (!noteService) return;
await noteService.deleteNote(id);
await loadNotes();
}, [noteService]);
return {
notes,
loading,
createNote,
updateNote,
deleteNote,
refreshNotes: loadNotes
};
}
11. 性能优化与最佳实践
11.1 批量操作优化
javascript
// ❌ 错误:循环中多次开启事务
async function addUsersBad(db, users) {
for (const user of users) {
await addUser(db, user); // 每个用户都开启新事务
}
}
// ✅ 正确:使用单次事务批量添加
async function addUsersBatch(db, users) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
users.forEach(user => {
store.add(user);
});
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}
11.2 使用 getAll() 代替游标遍历
javascript
// ✅ 获取所有数据时优先使用 getAll()
const allUsers = await store.getAll();
// 仅在需要条件过滤或分页时使用游标
11.3 合理设置索引
javascript
// 只为常用查询字段建立索引
// 索引会占用存储空间并影响写入性能
// ❌ 为每个字段都建立索引
userStore.createIndex('name', 'name');
userStore.createIndex('email', 'email');
userStore.createIndex('phone', 'phone');
userStore.createIndex('address', 'address');
userStore.createIndex('city', 'city');
// ✅ 只为核心查询字段建立索引
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('status', 'status');
11.4 使用 Promise 封装
javascript
// 统一封装为 Promise,便于 async/await 使用
function promisify(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 使用
const db = await promisify(indexedDB.open('mydb', 1));
11.5 错误处理与降级
javascript
class SafeIndexedDB {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
this.fallback = new Map(); // 降级方案使用 Map
}
async init() {
if (!window.indexedDB) {
console.warn('IndexedDB 不支持,使用内存存储');
return false;
}
try {
this.db = await this.openDB();
return true;
} catch (error) {
console.error('IndexedDB 初始化失败', error);
return false;
}
}
async get(storeName, key) {
if (!this.db) {
return this.fallback.get(`${storeName}_${key}`);
}
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
return promisify(store.get(key));
}
// 其他方法类似...
}
11.6 存储空间管理
javascript
// 检查存储使用情况
async function checkStorageQuota() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
const estimate = await navigator.storage.estimate();
const usageMB = estimate.usage / (1024 * 1024);
const quotaMB = estimate.quota / (1024 * 1024);
console.log(`已使用: ${usageMB.toFixed(2)} MB`);
console.log(`总配额: ${quotaMB.toFixed(2)} MB`);
console.log(`使用率: ${((usageMB / quotaMB) * 100).toFixed(2)}%`);
// 当使用率超过 80% 时清理旧数据
if (usageMB / quotaMB > 0.8) {
await cleanupOldData();
}
}
}
// 清理过期数据
async function cleanupOldData() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const transaction = db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
const index = store.index('updatedAt');
const range = IDBKeyRange.upperBound(thirtyDaysAgo);
const cursorRequest = index.openCursor(range);
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
cursor.delete(); // 删除过期数据
cursor.continue();
}
};
}
12. 常见问题与调试技巧
12.1 常见错误及解决方案
| 错误 | 原因 | 解决方案 |
|---|---|---|
DOMException: VersionError |
版本号低于当前版本 | 使用更高的版本号 |
DOMException: ConstraintError |
违反唯一约束 | 检查主键或唯一索引是否重复 |
DOMException: NotFoundError |
对象仓库或索引不存在 | 检查名称拼写或在升级回调中创建 |
DOMException: TransactionInactiveError |
事务已提交后操作 | 在事务生命周期内完成所有操作 |
DOMException: ReadOnlyError |
只读事务中尝试写入 | 使用 readwrite 模式 |
12.2 调试技巧
javascript
// 1. 在控制台查看数据库
// Chrome: Application → IndexedDB
// Firefox: Storage → IndexedDB
// 2. 监听所有请求
function debugRequest(request, operation) {
request.onsuccess = () => {
console.log(`[${operation}] 成功`, request.result);
};
request.onerror = () => {
console.error(`[${operation}] 失败`, request.error);
};
return request;
}
// 3. 查看所有数据
async function debugPrintAllData(db) {
const storeNames = Array.from(db.objectStoreNames);
for (const name of storeNames) {
const data = await db.transaction([name], 'readonly')
.objectStore(name)
.getAll();
console.log(`${name}:`, data);
}
}
12.3 数据迁移示例
javascript
// 版本升级时迁移数据
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
if (oldVersion < 2) {
// 升级到版本2:添加新字段
const store = db.objectStore('users');
const cursorRequest = store.openCursor();
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const user = cursor.value;
if (!user.createdAt) {
user.createdAt = new Date();
cursor.update(user);
}
cursor.continue();
}
};
}
if (oldVersion < 3) {
// 升级到版本3:创建新索引
const store = db.objectStore('users');
if (!store.indexNames.contains('city')) {
store.createIndex('city', 'city');
}
}
};
12.4 最佳实践总结
javascript
// 完整的最佳实践示例
class BestPracticeDB {
constructor(dbName) {
this.dbName = dbName;
this.db = null;
this.version = 1;
}
async open() {
if (this.db) return this.db;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
// 监听数据库关闭
this.db.onclose = () => {
console.warn('数据库连接关闭');
this.db = null;
};
// 监听数据库错误
this.db.onerror = (event) => {
console.error('数据库错误:', event.target.error);
};
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
this._createStores(db);
};
});
}
_createStores(db) {
// 只在升级时创建结构
if (!db.objectStoreNames.contains('data')) {
const store = db.createObjectStore('data', {
keyPath: 'id',
autoIncrement: true
});
// 只为常用查询创建索引
store.createIndex('timestamp', 'timestamp');
}
}
async transaction(storeName, mode, callback) {
const db = await this.open();
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], mode);
const store = transaction.objectStore(storeName);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
callback(store, resolve, reject);
});
}
async add(storeName, data) {
return this.transaction(storeName, 'readwrite', (store, resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(storeName, key) {
return this.transaction(storeName, 'readonly', (store, resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 批量操作
async bulkAdd(storeName, items) {
return this.transaction(storeName, 'readwrite', (store, resolve, reject) => {
let completed = 0;
const total = items.length;
items.forEach(item => {
const request = store.add(item);
request.onsuccess = () => {
completed++;
if (completed === total) resolve();
};
request.onerror = () => reject(request.error);
});
});
}
}
结语
IndexedDB 是前端存储大规模数据的强大工具。虽然 API 设计相对复杂,但通过合理的封装和最佳实践,可以构建出高性能、可靠的离线应用。
学习建议:
- 先掌握核心概念:数据库、对象仓库、索引、事务
- 使用 DevTools 实时查看数据,加深理解
- 从简单 CRUD 开始,逐步增加索引、游标等高级功能
- 在实际项目中尝试,比如构建离线待办事项或笔记应用
参考资料:
本文档为 IndexedDB 完整教程,涵盖从入门到实战的全部内容。建议按章节顺序学习,并动手实践每个示例。