7.2 IndexedDB 基础

7.2 IndexedDB 基础

IndexedDB是一种底层API,用于在客户端存储大量结构化数据。与Web Storage相比,IndexedDB提供了更强大的查询能力、事务支持和更大的存储空间(通常为浏览器可用磁盘空间的50%),适合构建需要本地持久化复杂数据的Web应用。

核心概念解析

1. 数据库架构

  • Database:顶层容器,每个源(协议+域名+端口)可以创建多个数据库
  • Object Store:相当于关系型数据库的表,存储键值对
  • Index:在对象存储上创建的辅助索引,支持高效查询
  • Transaction:原子操作单元,确保数据一致性
  • Cursor:遍历对象存储或索引的机制

2. 与关系型数据库术语对照

IndexedDB 关系型数据库
数据库(Database) 数据库(Database)
对象存储(Object Store) 表(Table)
索引(Index) 索引(Index)
游标(Cursor) 结果集(ResultSet)

基础操作指南

1. 打开/创建数据库

const request = indexedDB.open('MyDatabase', 2); // 名称+版本号

request.onerror = (event) => {
  console.error('数据库打开失败:', event.target.error);
};

request.onsuccess = (event) => {
  const db = event.target.result;
  console.log('数据库连接成功');
  // 保存db引用供后续使用
};

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // 创建对象存储(类似表)
  if (!db.objectStoreNames.contains('customers')) {
    const store = db.createObjectStore('customers', {
      keyPath: 'id',          // 主键
      autoIncrement: true     // 自增ID
    });
    
    // 创建索引
    store.createIndex('name_idx', 'name', { unique: false });
    store.createIndex('email_idx', 'email', { unique: true });
  }
  
  // 版本升级时可修改结构
  if (event.oldVersion < 2) {
    const store = event.currentTarget.transaction.objectStore('customers');
    store.createIndex('age_idx', 'age', { unique: false });
  }
};

2. CRUD操作

添加数据

function addCustomer(db, customer) {
  const transaction = db.transaction(['customers'], 'readwrite');
  const store = transaction.objectStore('customers');
  
  const request = store.add(customer);
  
  request.onsuccess = () => {
    console.log('客户数据已添加');
  };
  
  request.onerror = (event) => {
    console.error('添加失败:', event.target.error);
  };
}

查询数据

function getCustomer(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['customers'], 'readonly');
    const store = transaction.objectStore('customers');
    
    const request = store.get(id);
    
    request.onsuccess = () => {
      resolve(request.result);
    };
    
    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

// 使用索引查询
function getCustomerByName(db, name) {
  const transaction = db.transaction(['customers'], 'readonly');
  const store = transaction.objectStore('customers');
  const index = store.index('name_idx');
  
  const request = index.get(name);
  
  request.onsuccess = () => {
    console.log('查询结果:', request.result);
  };
}

更新数据

function updateCustomer(db, customer) {
  const transaction = db.transaction(['customers'], 'readwrite');
  const store = transaction.objectStore('customers');
  
  const request = store.put(customer); // put方法自动更新
  
  request.onsuccess = () => {
    console.log('数据已更新');
  };
}

删除数据

function deleteCustomer(db, id) {
  const transaction = db.transaction(['customers'], 'readwrite');
  const store = transaction.objectStore('customers');
  
  const request = store.delete(id);
  
  request.onsuccess = () => {
    console.log('数据已删除');
  };
}

高级查询技术

1. 使用游标遍历数据

function getAllCustomers(db) {
  const transaction = db.transaction(['customers'], 'readonly');
  const store = transaction.objectStore('customers');
  
  const customers = [];
  
  const request = store.openCursor();
  
  request.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      customers.push(cursor.value);
      cursor.continue();
    } else {
      console.log('获取所有客户:', customers);
    }
  };
}

2. 范围查询

// 查询年龄在18-30之间的客户
function getCustomersByAgeRange(db, min, max) {
  const transaction = db.transaction(['customers'], 'readonly');
  const store = transaction.objectStore('customers');
  const index = store.index('age_idx');
  
  const range = IDBKeyRange.bound(min, max);
  const customers = [];
  
  const request = index.openCursor(range);
  
  request.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      customers.push(cursor.value);
      cursor.continue();
    } else {
      console.log('年龄范围内的客户:', customers);
    }
  };
}

3. 复合查询模拟

// 通过多个索引筛选数据
function queryCustomers(db, conditions) {
  return new Promise((resolve) => {
    const transaction = db.transaction(['customers'], 'readonly');
    const store = transaction.objectStore('customers');
    
    const request = store.openCursor();
    const results = [];
    
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        const customer = cursor.value;
        let match = true;
        
        // 手动检查每个条件
        for (const [key, value] of Object.entries(conditions)) {
          if (customer[key] !== value) {
            match = false;
            break;
          }
        }
        
        if (match) results.push(customer);
        cursor.continue();
      } else {
        resolve(results);
      }
    };
  });
}

事务管理

1. 事务基础使用

function transferFunds(db, fromId, toId, amount) {
  const transaction = db.transaction(['accounts'], 'readwrite');
  const store = transaction.objectStore('accounts');
  
  // 获取源账户
  const fromRequest = store.get(fromId);
  
  fromRequest.onsuccess = () => {
    const fromAccount = fromRequest.result;
    if (fromAccount.balance < amount) {
      throw new Error('余额不足');
    }
    
    // 获取目标账户
    const toRequest = store.get(toId);
    
    toRequest.onsuccess = () => {
      const toAccount = toRequest.result;
      
      // 更新余额
      fromAccount.balance -= amount;
      toAccount.balance += amount;
      
      // 保存更新
      store.put(fromAccount);
      store.put(toAccount);
    };
  };
  
  transaction.oncomplete = () => {
    console.log('转账完成');
  };
  
  transaction.onerror = (event) => {
    console.error('转账失败:', event.target.error);
  };
}

2. 事务隔离与模式

IndexedDB支持三种事务模式:

  • readonly:只读,可并发执行
  • readwrite:读写,避免与其他读写事务同时执行
  • versionchange:数据库结构变更
// 并发事务示例
function concurrentOperations(db) {
  // 这两个事务可以并发执行
  const readTx1 = db.transaction(['products'], 'readonly');
  const readTx2 = db.transaction(['products'], 'readonly');
  
  // 这个事务会等待前面的读写事务完成
  const writeTx = db.transaction(['products'], 'readwrite');
}

实用封装示例

1. 通用数据库操作类

class IDBHelper {
  constructor(dbName, version, upgradeCallback) {
    this.dbPromise = new Promise((resolve, reject) => {
      const request = indexedDB.open(dbName, version);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);
      request.onupgradeneeded = (event) => {
        upgradeCallback(event.target.result, event.oldVersion);
      };
    });
  }
  
  async get(storeName, key) {
    const db = await this.dbPromise;
    return new Promise((resolve, reject) => {
      const tx = db.transaction(storeName, 'readonly');
      const store = tx.objectStore(storeName);
      const request = store.get(key);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);
    });
  }
  
  async getAll(storeName, indexName, range) {
    const db = await this.dbPromise;
    return new Promise((resolve, reject) => {
      const tx = db.transaction(storeName, 'readonly');
      const store = tx.objectStore(storeName);
      const target = indexName ? store.index(indexName) : store;
      const request = target.getAll(range);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);
    });
  }
  
  // 其他方法类似实现...
}

// 使用示例
const dbHelper = new IDBHelper('MyAppDB', 1, (db, oldVersion) => {
  if (!db.objectStoreNames.contains('todos')) {
    db.createObjectStore('todos', { keyPath: 'id', autoIncrement: true });
  }
});

async function addTodo(todo) {
  const db = await dbHelper.dbPromise;
  return new Promise((resolve, reject) => {
    const tx = db.transaction('todos', 'readwrite');
    const store = tx.objectStore('todos');
    const request = store.add(todo);
    
    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);
  });
}

性能优化策略

  1. 批量操作优化

    async function bulkAdd(storeName, items) {
      const db = await dbHelper.dbPromise;
      const tx = db.transaction(storeName, 'readwrite');
      const store = tx.objectStore(storeName);
      
      items.forEach(item => {
        store.add(item);
      });
      
      return new Promise((resolve, reject) => {
        tx.oncomplete = () => resolve();
        tx.onerror = () => reject(tx.error);
      });
    }
    
  2. 游标分批处理

    function processLargeDataset(db, batchSize, processCallback) {
      const transaction = db.transaction(['largeData'], 'readonly');
      const store = transaction.objectStore('largeData');
      let processed = 0;
      
      function processBatch(cursor) {
        let count = 0;
        while (cursor && count < batchSize) {
          processCallback(cursor.value);
          count++;
          processed++;
          cursor.continue();
        }
        
        if (cursor) {
          console.log(`已处理 ${processed} 条记录`);
          // 使用setTimeout避免阻塞主线程
          setTimeout(() => {
            cursor.continue();
          }, 0);
        }
      }
      
      store.openCursor().onsuccess = (event) => {
        processBatch(event.target.result);
      };
    }
    
  3. 索引设计原则

    • 为常用查询条件创建索引
    • 避免过多索引影响写入性能
    • 对唯一性字段设置unique约束

错误处理与调试

  1. 常见错误类型

    • ConstraintError:违反唯一约束
    • TransactionInactiveError:事务已结束
    • QuotaExceededError:超出存储配额
  2. 调试技巧

    // 添加全面的错误处理
    transaction.onerror = (event) => {
      console.error('事务错误:', event.target.error.name, 
                   event.target.error.message);
      
      // 特定错误处理
      if (event.target.error.name === 'QuotaExceededError') {
        showStorageFullWarning();
      }
    };
    
  3. 浏览器开发者工具

    • Chrome: Application → IndexedDB
    • Firefox: Storage → IndexedDB
    • Edge: Debugger → IndexedDB

现代封装库推荐

  1. Dexie.js

    const db = new Dexie('FriendsDB');
    db.version(1).stores({
      friends: '++id, name, age'
    });
    
    async function addFriend() {
      await db.friends.add({
        name: '张三',
        age: 25,
      });
    }
    
  2. localForage

    localforage.config({
      driver: [localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE],
      name: 'MyApp'
    });
    
    localforage.setItem('key', 'value').then(() => {
      return localforage.getItem('key');
    }).then(value => {
      console.log(value);
    });
    

IndexedDB作为浏览器端最强大的存储方案之一,虽然API较为底层,但通过合理封装和现代工具库的帮助,完全可以成为开发复杂Web应用的可靠数据存储层。掌握其核心概念和最佳实践,能够显著提升应用的离线能力和响应速度。

#前端开发 分享于 2025-05-20

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】