IndexedDB

IndexedDB是浏览器提供的NoSQL数据库,用于在客户端存储大量结构化数据。本章将介绍IndexedDB的基本概念和使用方法。

IndexedDB概述

什么是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;
}

CRUD操作封装

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
});

IDBKeyRange

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
});

存储复杂数据

存储Blob和File

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使用建议

  1. 合理设计索引:索引加速查询但增加写入开销
  2. 批量操作:使用事务批量处理数据
  3. 错误处理:始终处理错误和异常
  4. 版本管理:谨慎处理数据库升级
  5. 数据清理:定期清理不需要的数据

🔍 查询优化技巧

  • 使用索引查询而非全表扫描
  • 合理使用IDBKeyRange缩小范围
  • 分页查询避免一次加载过多数据
  • 复杂查询考虑使用多个索引组合

下一步

下一章将探讨 Canvas绘图,学习如何在网页中进行2D图形绘制。