IndexedDB是浏览器提供的NoSQL数据库,用于在客户端存储大量结构化数据。本章将介绍IndexedDB的基本概念和使用方法。
const indexedDBOverview = {
definition: 'IndexedDB是一个事务型数据库系统,类似于基于SQL的RDBMS',
features: [
'存储大量数据(无固定限制)',
'支持索引,快速查询',
'事务支持,保证数据一致性',
'异步API,不阻塞主线程',
'支持二进制数据(Blob、File)',
'同源策略保护'
],
comparison: {
localStorage: {
capacity: '~5MB',
type: '字符串键值对',
async: false,
query: '无索引'
},
IndexedDB: {
capacity: '无固定限制(通常>50MB)',
type: '结构化数据',
async: true,
query: '支持索引查询'
}
}
};
const databaseStructure = {
database: {
description: '数据库,包含多个对象存储',
example: 'myAppDB'
},
objectStore: {
description: '对象存储,类似于表',
example: 'users, products, orders'
},
index: {
description: '索引,加速查询',
example: 'email索引、name索引'
},
record: {
description: '记录,存储的数据项',
example: '{ id: 1, name: "张三", email: "..." }'
},
key: {
description: '主键,唯一标识记录',
types: ['内联键', '外部键', '自动生成']
}
};
function openDatabase(dbName, version, onUpgrade) {
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 = request.result;
onUpgrade(db, event.oldVersion, event.newVersion);
};
});
}
async function initDatabase() {
const db = await openDatabase('myApp', 1, (db, oldVersion, newVersion) => {
if (!db.objectStoreNames.contains('users')) {
const userStore = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('name', 'name', { unique: false });
userStore.createIndex('age', 'age', { unique: false });
}
if (!db.objectStoreNames.contains('products')) {
const productStore = db.createObjectStore('products', {
keyPath: 'id'
});
productStore.createIndex('category', 'category', { unique: false });
productStore.createIndex('price', 'price', { unique: false });
}
if (!db.objectStoreNames.contains('orders')) {
const orderStore = db.createObjectStore('orders', {
keyPath: 'id',
autoIncrement: true
});
orderStore.createIndex('userId', 'userId', { unique: false });
orderStore.createIndex('status', 'status', { unique: false });
orderStore.createIndex('createdAt', 'createdAt', { unique: false });
}
});
return db;
}
class IndexedDBHelper {
constructor(dbName, version, onUpgrade) {
this.dbName = dbName;
this.version = version;
this.onUpgrade = onUpgrade;
this.db = null;
}
async connect() {
if (this.db) return this.db;
this.db = await openDatabase(this.dbName, this.version, this.onUpgrade);
return this.db;
}
async add(storeName, data) {
const db = await this.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(storeName, key) {
const db = await this.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(storeName) {
const db = await this.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async update(storeName, data) {
const db = await this.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async delete(storeName, key) {
const db = await this.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async clear(storeName) {
const db = await this.connect();
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);
});
}
async count(storeName, query) {
const db = await this.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.count(query);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
const dbHelper = new IndexedDBHelper('myApp', 1, (db) => {
if (!db.objectStoreNames.contains('users')) {
db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
}
});
await dbHelper.add('users', { name: '张三', email: 'zhangsan@example.com' });
const user = await dbHelper.get('users', 1);
const allUsers = await dbHelper.getAll('users');
await dbHelper.update('users', { id: 1, name: '李四', email: 'lisi@example.com' });
await dbHelper.delete('users', 1);
class IndexedDBQuery {
constructor(dbHelper) {
this.dbHelper = dbHelper;
}
async getByIndex(storeName, indexName, value) {
const db = await this.dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const request = index.get(value);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAllByIndex(storeName, indexName, value) {
const db = await this.dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const request = index.getAll(value);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getByIndexRange(storeName, indexName, lower, upper) {
const db = await this.dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const range = IDBKeyRange.bound(lower, upper);
const request = index.getAll(range);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getRange(storeName, options = {}) {
const db = await this.dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const target = options.index
? store.index(options.index)
: store;
let range = null;
if (options.lower !== undefined || options.upper !== undefined) {
const lower = options.lower;
const upper = options.upper;
const lowerOpen = options.lowerOpen || false;
const upperOpen = options.upperOpen || false;
if (lower !== undefined && upper !== undefined) {
range = IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen);
} else if (lower !== undefined) {
range = IDBKeyRange.lowerBound(lower, lowerOpen);
} else {
range = IDBKeyRange.upperBound(upper, upperOpen);
}
}
const direction = options.reverse ? 'prev' : 'next';
const request = target.openCursor(range, direction);
const results = [];
let count = 0;
const limit = options.limit || Infinity;
const offset = options.offset || 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && count < limit) {
if (offset > 0) {
cursor.advance(offset);
return;
}
results.push(cursor.value);
count++;
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
}
const query = new IndexedDBQuery(dbHelper);
const user = await query.getByIndex('users', 'email', 'zhangsan@example.com');
const adults = await query.getByIndexRange('users', 'age', 18, 65);
const recentOrders = await query.getRange('orders', {
index: 'createdAt',
lower: Date.now() - 7 * 24 * 60 * 60 * 1000,
limit: 10,
reverse: true
});
const keyRangeExamples = {
only: {
description: '匹配单个值',
code: 'IDBKeyRange.only(5)',
matches: [5]
},
lowerBound: {
description: '大于等于某值',
code: 'IDBKeyRange.lowerBound(10)',
matches: [10, 11, 12, ...]
},
upperBound: {
description: '小于等于某值',
code: 'IDBKeyRange.upperBound(100)',
matches: [..., 98, 99, 100]
},
bound: {
description: '范围查询',
code: 'IDBKeyRange.bound(10, 100)',
matches: [10, 11, ..., 99, 100]
},
boundExclusive: {
description: '开区间查询',
code: 'IDBKeyRange.bound(10, 100, true, true)',
matches: [11, 12, ..., 98, 99]
}
};
const rangeExamples = {
exactMatch: IDBKeyRange.only('张三'),
greaterOrEqual: IDBKeyRange.lowerBound(18),
greaterThan: IDBKeyRange.lowerBound(18, true),
lessOrEqual: IDBKeyRange.upperBound(100),
lessThan: IDBKeyRange.upperBound(100, true),
between: IDBKeyRange.bound(18, 65),
betweenExclusive: IDBKeyRange.bound(18, 65, true, true)
};
const transactionTypes = {
readonly: {
description: '只读事务,可并发执行',
use: '查询数据'
},
readwrite: {
description: '读写事务,需要锁定',
use: '增删改数据'
},
versionchange: {
description: '版本变更事务',
use: '创建/删除对象存储和索引'
}
};
async function transactionExample() {
const db = await dbHelper.connect();
const transaction = db.transaction(['users', 'orders'], 'readwrite');
transaction.oncomplete = () => {
console.log('事务完成');
};
transaction.onerror = () => {
console.error('事务失败:', transaction.error);
};
transaction.onabort = () => {
console.log('事务已回滚');
};
const userStore = transaction.objectStore('users');
const orderStore = transaction.objectStore('orders');
userStore.add({ name: '张三', email: 'zhangsan@example.com' });
orderStore.add({ userId: 1, product: '商品A', quantity: 2 });
}
class TransactionManager {
constructor(dbHelper) {
this.dbHelper = dbHelper;
}
async run(storeNames, mode, operations) {
const db = await this.dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeNames, mode);
const stores = {};
storeNames.forEach(name => {
stores[name] = transaction.objectStore(name);
});
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(new Error('事务已中止'));
operations(stores, transaction);
});
}
async runWithResult(storeNames, mode, operations) {
const db = await this.dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeNames, mode);
const stores = {};
let result;
storeNames.forEach(name => {
stores[name] = transaction.objectStore(name);
});
transaction.oncomplete = () => resolve(result);
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(new Error('事务已中止'));
result = operations(stores, transaction);
});
}
}
const txManager = new TransactionManager(dbHelper);
await txManager.run(['users', 'orders'], 'readwrite', (stores) => {
stores.users.add({ name: '张三' });
stores.orders.add({ userId: 1, product: '商品A' });
});
const orderWithUser = await txManager.runWithResult(
['users', 'orders'],
'readonly',
(stores) => {
const order = stores.orders.get(1);
const user = stores.users.get(order.result.userId);
return { order: order.result, user: user.result };
}
);
async function iterateWithCursor(storeName, callback) {
const db = await dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
callback(cursor.value, cursor);
cursor.continue();
} else {
resolve();
}
};
request.onerror = () => reject(request.error);
});
}
await iterateWithCursor('users', (user, cursor) => {
console.log(user.name);
});
async function updateWithCursor(storeName, predicate, updater) {
const db = await dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.openCursor();
let updated = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (predicate(cursor.value)) {
const updatedValue = updater(cursor.value);
cursor.update(updatedValue);
updated++;
}
cursor.continue();
} else {
resolve(updated);
}
};
request.onerror = () => reject(request.error);
});
}
await updateWithCursor('users',
user => user.age < 18,
user => ({ ...user, isMinor: true })
);
async function paginate(storeName, options = {}) {
const {
index,
keyRange,
direction = 'next',
limit = 10,
offset = 0,
reverse = false
} = options;
const db = await dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const target = index ? store.index(index) : store;
const cursorDirection = reverse ? 'prev' : 'next';
const request = target.openCursor(keyRange, cursorDirection);
const results = [];
let skipped = 0;
let hasMore = false;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
resolve({ results, hasMore: false });
return;
}
if (skipped < offset) {
skipped++;
cursor.continue();
return;
}
if (results.length < limit) {
results.push(cursor.value);
cursor.continue();
} else {
hasMore = true;
resolve({ results, hasMore });
}
};
request.onerror = () => reject(request.error);
});
}
const page1 = await paginate('users', { limit: 10, offset: 0 });
const page2 = await paginate('users', { limit: 10, offset: 10 });
const recentOrders = await paginate('orders', {
index: 'createdAt',
reverse: true,
limit: 20
});
async function storeFile(file, metadata = {}) {
const db = await dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction('files', 'readwrite');
const store = transaction.objectStore('files');
const fileRecord = {
name: file.name,
type: file.type,
size: file.size,
blob: file,
metadata,
createdAt: Date.now()
};
const request = store.add(fileRecord);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function retrieveFile(id) {
const db = await dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction('files', 'readonly');
const store = transaction.objectStore('files');
const request = store.get(id);
request.onsuccess = () => {
const record = request.result;
if (record) {
const url = URL.createObjectURL(record.blob);
resolve({ ...record, url });
} else {
resolve(null);
}
};
request.onerror = () => reject(request.error);
});
}
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
const id = await storeFile(file, { uploadedBy: 'user1' });
console.log('文件已存储,ID:', id);
});
async function bulkInsert(storeName, items, batchSize = 1000) {
const db = await dbHelper.connect();
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
transaction.oncomplete = resolve;
transaction.onerror = () => reject(transaction.error);
batch.forEach(item => store.add(item));
});
console.log(`已插入 ${Math.min(i + batchSize, items.length)}/${items.length}`);
}
}
async function bulkDelete(storeName, predicate) {
const db = await dbHelper.connect();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.openCursor();
let deleted = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (predicate(cursor.value)) {
cursor.delete();
deleted++;
}
cursor.continue();
} else {
resolve(deleted);
}
};
request.onerror = () => reject(request.error);
});
}
💾 IndexedDB使用建议
- 合理设计索引:索引加速查询但增加写入开销
- 批量操作:使用事务批量处理数据
- 错误处理:始终处理错误和异常
- 版本管理:谨慎处理数据库升级
- 数据清理:定期清理不需要的数据
🔍 查询优化技巧
- 使用索引查询而非全表扫描
- 合理使用IDBKeyRange缩小范围
- 分页查询避免一次加载过多数据
- 复杂查询考虑使用多个索引组合
下一章将探讨 Canvas绘图,学习如何在网页中进行2D图形绘制。