IndexedDB 是 HTML5 引入的一种高性能、事务型的客户端存储 API,用于在浏览器中存储大量结构化数据。它提供了比 localStorage 更强大的存储能力和更丰富的查询接口。

一、IndexedDB 简介

1.1 什么是 IndexedDB?

IndexedDB 是一种 NoSQL 数据库,允许在浏览器中存储大量结构化数据(如对象、数组等),并提供高性能的索引和查询功能。

1.2 主要特性
  • 异步操作:所有操作都是异步的,避免阻塞主线程。

  • 事务支持:确保数据操作的原子性、一致性、隔离性和持久性。

  • 索引支持:可以为数据创建索引,实现高效查询。

  • 大容量存储:存储空间通常远大于 localStorage(一般为几百 MB 甚至 GB 级别)。

  • 结构化数据:支持存储复杂的 JavaScript 对象。

  • 同源策略:遵循同源策略,确保数据安全。

二、IndexedDB 架构与核心概念

2.1 数据模型

IndexedDB 采用对象存储模型,与关系型数据库有以下对应关系:

关系型数据库

IndexedDB概念

数据库(Database)

数据库(Database)

表(Table)

对象存储空间(Object Store)

表字段(Column)

对象属性(Object Property)

表记录(Row)

对象(Object)

主键(Primary Key)

键路径(Key Path)

索引(Index)

索引(Index)

IndexedDB的索引与关系型数据库表字段的概念对比

特性

关系型数据库(如MySQL)

IndexedDB

表字段

表的结构组成部分,定义数据的类型和属性(id、name、email),存储实际数据

对象存储空间中对象的属性({id: 1, name: "august", email: "august@test.com"}中的id、name、email)相当于表字段,存储实际数据.

索引

基于表字段创建的优化结构,不存储实际数据,仅存储指向数据的引用,用于加速查询

基于对象属性创建的优化结构,不存储实际数据,仅存储属性值和指向对应对象的引用,用于加速查询

IndexedDB的索引与关系型数据库表字段的核心区别

区别点

关系型数据库的表字段

IndexedDB的索引

作用

定义数据结构,存储实际数据内容

加速基于特定属性的查询操作,提高检索效率

必要性

表必须有字段,否则无法存储数据

索引是可选的,对象存储空间可以没有索引(但查询效率会降低)

存储内容

存储实际数据值(如字符串、数字等)

存储属性值和指向对应对象的引用(指针)

唯一性约束

可通过UNIQUE约束限制字段值的唯一性

可通过{ unique: true }配置限制索引属性值的唯一性

2.2 IndexedDB 核心概念
2.2.1 数据库 (Database)
  • IndexedDB 中的最高层级存储结构。

  • 每个数据库有一个名称和版本号。

  • 版本号必须是整数,升级数据库时版本号必须递增。

2.2.2 对象存储空间 (Object Store)
  • 数据库中的表结构,用于存储数据。

  • 每个对象存储空间必须有一个主键 (key)。

  • 主键可以是自增的,也可以由开发者指定。

2.2.3 索引 (Index)
  • 基于对象存储空间中的某个属性创建的索引。

  • 用于加速数据查询。

  • 可以创建单键索引或复合索引。

  • 支持唯一索引和非唯一索引。

2.2.4 事务 (Transaction)
  • 用于确保数据操作的原子性。

  • 所有数据操作必须在事务中执行。

  • 事务有三种模式:只读 (readonly)、读写 (readwrite) 和版本变更 (versionchange)。

2.2.5 游标 (Cursor)
  • 用于遍历对象存储空间或索引中的数据。

  • 支持正向和反向遍历。

  • 可以设置范围和限制条件。

2.3 对象存储的两种键模式

内联键 (Inline Key)

// 使用对象的某个属性作为主键
const objectStore = db.createObjectStore('users', { keyPath: 'id' });

外键 (Out-of-line Key)

// 不使用对象属性作为主键,存储时单独指定
const objectStore = db.createObjectStore('users', { autoIncrement: true });

// 存储时指定键
objectStore.add(userData, 123);
2.4 索引类型

单属性索引

objectStore.createIndex('email', 'email', { unique: true });

复合索引

// 复合索引只能在现代浏览器中使用
objectStore.createIndex('name_age', ['name', 'age'], { unique: false });

三、IndexedDB 基本操作

3.1 打开/创建数据库的一般流程

3.1.1 数据库打开/创建请求

const request = indexedDB.open('myDatabase', 1);
  • 函数:indexedDB.open();

  • 作用:打开或创建一个名为"myDatabase"的IndexedDB数据库。

  • 参数:

    • 第一个参数:数据库名称 "myDatabase"。

    • 第二个参数:数据库版本号 1。

  • 返回值:一个IDBOpenDBRequest对象,用于处理异步操作的结果。

3.1.2 数据库升级事件处理

request.onupgradeneeded = function(event) {
  const db = event.target.result;
  
  //创建对象存储空间
  const objectStore = db.createObjectStore('users', { keyPath: 'id' });
  
  //创建索引
  objectStore.createIndex('name', 'name', { unique: false });
  objectStore.createIndex('email', 'email', { unique: true });
};
  • 事件:onupgradeneeded。

  • 触发条件:当数据库首次创建或版本号高于现有版本时触发。

  • 功能:

    • 通过 event.target.result 获取数据库实例。

    • 创建名为"users"的对象存储空间(类似于数据库表)。

      • 参数:名称"users",配置项{ keyPath: 'id' }表示使用对象的id属性作为主键。

  • 为对象存储空间创建索引:

    • name索引:非唯一,用于快速查询name字段。

    • email索引:唯一,确保email字段的值不重复。

3.1.3 数据库打开成功事件处理

request.onsuccess = function(event) {
  const db = event.target.result;
  console.log('数据库打开成功');
  
  //在这里可以进行数据操作
};
  • 事件:onsuccess

  • 触发条件:数据库成功打开时触发

  • 功能:

    • 通过 event.target.result 获取数据库实例。

    • 可以在这个回调函数中进行后续的数据操作(如增删改查)。

3.1.4 数据库打开失败事件处理

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

  • 触发条件:数据库打开失败时触发。

  • 功能:

    • 通过 event.target.error 获取具体的错误对象。

上述分解操作的完整示例代码:

// 打开或创建名为 "myDatabase" 的数据库,版本号为 1
const request = indexedDB.open('myDatabase', 1);

// 数据库升级或首次创建时触发
request.onupgradeneeded = function(event) {
  const db = event.target.result;
  
  // 创建对象存储空间
  const objectStore = db.createObjectStore('users', { keyPath: 'id' });
  
  // 创建索引
  objectStore.createIndex('name', 'name', { unique: false });
  objectStore.createIndex('email', 'email', { unique: true });
};

// 数据库打开成功时触发
request.onsuccess = function(event) {
  const db = event.target.result;
  console.log('数据库打开成功');
  
  // 在这里可以进行数据操作
};

// 数据库打开失败时触发
request.onerror = function(event) {
  console.error('数据库打开失败:', event.target.error);
};
3.2 添加数据
// 向IndexedDB数据库的users对象存储空间添加用户数据
// db: 已经成功打开的IndexedDB数据库实例
// user: 要添加的用户对象,必须包含id字段(作为主键)
function addUser(db, user) {
  // 创建读写事务
  // 第一个参数:数组,指定要操作的对象存储空间(表),这里是['users']
  // 第二个参数:事务模式,'readwrite'表示可读可写(查询时可用'readonly')
  const transaction = db.transaction(['users'], 'readwrite');
  // transaction.objectStore():通过事务获取指定的对象存储空间实例
  const objectStore = transaction.objectStore('users');
  
  // 添加数据
  // 参数:要添加的用户对象
  // 返回值:请求对象(IDBRequest),用于处理异步操作结果
  // 特性:如果主键(id)已存在,会抛出错误(这是与put()方法的主要区别)
  const request = objectStore.add(user);
  
  // request.onsuccess:添加成功时触发的回调函数
  request.onsuccess = function() {
    console.log('用户添加成功');
  };
  
  // request.onerror:添加失败时触发的回调函数
  request.onerror = function() {
    // 通过request.error获取具体错误信息
    console.error('用户添加失败:', request.error);
  };
  
  // transaction.oncomplete:当整个事务中的所有操作都完成时触发
  // 注意:这是事务级别的回调,不是单个请求的回调
  transaction.oncomplete = function() {
    console.log('事务完成');
  };
}

// 使用示例
const user = { id: 1, name: 'august', email: 'august@example.com' };
addUser(db, user);
3.3 读取数据

根据主键读取

// 根据主键读取
function getUserById(db, id) {
  const transaction = db.transaction(['users'], 'readonly');
  const objectStore = transaction.objectStore('users');
  
  const request = objectStore.get(id);
  
  request.onsuccess = function() {
    if (request.result) {
      console.log('找到用户:', request.result);
    } else {
      console.log('未找到用户');
    }
  };
}

使用索引读取

// 使用索引读取
function getUserByEmail(db, email) {
  const transaction = db.transaction(['users'], 'readonly');
  const objectStore = transaction.objectStore('users');
  const index = objectStore.index('email');
  
  const request = index.get(email);
  
  request.onsuccess = function() {
    if (request.result) {
      console.log('找到用户:', request.result);
    } else {
      console.log('未找到用户');
    }
  };
}
3.4 更新数据
function updateUser(db, user) {
  const transaction = db.transaction(['users'], 'readwrite');
  const objectStore = transaction.objectStore('users');
  
  const request = objectStore.put(user);
  
  request.onsuccess = function() {
    console.log('用户更新成功');
  };
  
  request.onerror = function() {
    console.error('用户更新失败:', request.error);
  };
}

// 使用示例
const updatedUser = { id: 1, name: '八月', email: 'bayue@new.com' };
updateUser(db, updatedUser);
3.5 删除数据
function deleteUser(db, id) {
  const transaction = db.transaction(['users'], 'readwrite');
  const objectStore = transaction.objectStore('users');
  
  const request = objectStore.delete(id);
  
  request.onsuccess = function() {
    console.log('用户删除成功');
  };
  
  request.onerror = function() {
    console.error('用户删除失败:', request.error);
  };
}

// 使用示例
deleteUser(db, 1);
3.6 使用游标遍历数据
function getAllUsers(db) {
  const transaction = db.transaction(['users'], 'readonly');
  const objectStore = transaction.objectStore('users');
  const users = [];
  
  // 打开游标
  const request = objectStore.openCursor();
  
  request.onsuccess = function(event) {
    const cursor = event.target.result;
    
    if (cursor) {
      users.push(cursor.value);
      cursor.continue(); // 移动到下一条记录
    } else {
      console.log('所有用户:', users);
    }
  };
}

// 使用索引和游标查询
function getUsersByName(db, name) {
  const transaction = db.transaction(['users'], 'readonly');
  const objectStore = transaction.objectStore('users');
  const index = objectStore.index('name');
  const users = [];
  
  // 打开索引游标,只查找name为指定值的记录
  const request = index.openCursor(IDBKeyRange.only(name));
  
  request.onsuccess = function(event) {
    const cursor = event.target.result;
    
    if (cursor) {
      users.push(cursor.value);
      cursor.continue();
    } else {
      console.log('找到用户:', users);
    }
  };
}

四、高级特性

4.1 事务管理
// 事务错误处理
const transaction = db.transaction(['users'], 'readwrite');

transaction.oncomplete = function() {
  console.log('事务完成');
};

transaction.onerror = function() {
  console.error('事务失败:', transaction.error);
};

transaction.onabort = function() {
  console.log('事务被中止');
};

// 手动中止事务
// transaction.abort();
4.2索引范围查找
function getUsersByAgeRange(db, minAge, maxAge) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const objectStore = transaction.objectStore('users');
    // objectStore.index('age'):获取users对象存储空间中名为age的索引
    // 前提:数据库初始化时已为age属性创建了索引(如objectStore.createIndex('age', 'age', { unique: false }))
    const index = objectStore.index('age');
    const users = [];
    
    // 创建一个范围对象,表示年龄在minAge到maxAge之间(包含边界值)
    // 其他范围方法:only()(精确匹配)、lowerBound()(大于等于)、upperBound()(小于等于)
    const range = IDBKeyRange.bound(minAge, maxAge);
    // index.openCursor(range):使用索引打开一个游标,仅遍历年龄在指定范围内的记录
    // 索引游标的优势:直接基于索引值排序,无需全表扫描,查询效率更高
    const request = index.openCursor(range);
    
    request.onsuccess = function(event) {
      const cursor = event.target.result;
      if (cursor) {
        users.push(cursor.value);
        cursor.continue();
      } else {
        resolve(users);  // 成功时返回结果
      }
    };
    
    request.onerror = function(event) {
      reject(event.target.error);  // 失败时返回错误
    };
  });
}

// 使用示例
getUsersByAgeRange(db, 20, 30)
  .then(users => console.log('找到用户:', users))
  .catch(error => console.error('查询失败:', error));
4.3 数据库版本升级
// 打开或创建数据库(版本1)
const request = indexedDB.open('myDatabase', 2); // 直接请求版本2,触发升级

// 数据库升级或首次创建时触发
request.onupgradeneeded = function(event) {
  const db = event.target.result;
  const transaction = event.target.transaction; // 使用事件自带的事务
  
  console.log(`数据库升级: 从版本 ${event.oldVersion} 到 ${event.newVersion}`);
  
  // 版本1:创建初始数据库结构
  if (event.oldVersion < 1) {
    // 创建users对象存储空间,使用id作为主键
    const userStore = db.createObjectStore('users', { keyPath: 'id' });
    
    // 为name和email字段创建索引
    userStore.createIndex('name', 'name', { unique: false });
    userStore.createIndex('email', 'email', { unique: true });
    
    console.log('版本1:创建了users对象存储空间和基本索引');
  }
  
  // 版本2:添加age索引
  if (event.oldVersion < 2) {
    // 获取users对象存储空间
    const userStore = transaction.objectStore('users');
    
    // 为age字段创建索引
    userStore.createIndex('age', 'age', { unique: false });
    
    console.log('版本2:为users对象存储空间添加了age索引');
  }
  
  // 版本3:如果未来需要更多升级,可以继续添加
  // if (event.oldVersion < 3) {
  //   // 新的升级逻辑
  // }
};

// 数据库打开成功时触发
request.onsuccess = function(event) {
  const db = event.target.result;
  console.log('数据库打开成功,当前版本:', db.version);
  
  // 数据库操作示例:添加一个带age字段的用户
  addUserWithAge(db, { id: 1, name: 'September', email: 'september@example.com', age: 30 });
  
  // 关闭数据库(实际应用中可根据需要决定何时关闭)
  // db.close();
};

// 数据库打开失败时触发
request.onerror = function(event) {
  console.error('数据库操作失败:', event.target.error);
};

// 添加带age字段的用户示例
function addUserWithAge(db, user) {
  const transaction = db.transaction(['users'], 'readwrite');
  const userStore = transaction.objectStore('users');
  
  const addRequest = userStore.put(user); // 使用put,支持添加和更新
  
  addRequest.onsuccess = function() {
    console.log('用户添加/更新成功:', user);
  };
  
  addRequest.onerror = function() {
    console.error('用户添加/更新失败:', addRequest.error);
  };
}

五、高级查询技术

5.1 键范围 (IDBKeyRange)

精确匹配

// 精确匹配
const exactRange = IDBKeyRange.only('value');

大于等于

// 大于等于
const lowerBoundRange = IDBKeyRange.lowerBound(10);

大于

// 大于
const lowerBoundOpenRange = IDBKeyRange.lowerBound(10, true);

小于等于

// 小于等于
const upperBoundRange = IDBKeyRange.upperBound(100);

小于

// 小于
const upperBoundOpenRange = IDBKeyRange.upperBound(100, true);

范围匹配(闭区间)

// 范围匹配(闭区间)
const boundRange = IDBKeyRange.bound(10, 100);

范围匹配(开区间)

// 范围匹配(开区间)
const boundOpenRange = IDBKeyRange.bound(10, 100, true, true);
5.2 高级游标操作
5.2.1 方向控制

正向遍历(默认)

// 正向遍历(默认)
objectStore.openCursor(range, 'next');

正向遍历,跳过重复键

// 正向遍历,跳过重复键
objectStore.openCursor(range, 'nextunique');

反向遍历

// 反向遍历
objectStore.openCursor(range, 'prev');

反向遍历,跳过重复键

// 反向遍历,跳过重复键
objectStore.openCursor(range, 'prevunique');
5.2.2 游标分页
function getUsersPage(db, pageSize, pageNumber) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const objectStore = transaction.objectStore('users');
    const users = [];
    const skipCount = pageSize * (pageNumber - 1);
    let count = 0;

    const request = objectStore.openCursor();
    
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (cursor) {
        if (count >= skipCount && count < skipCount + pageSize) {
          users.push(cursor.value);
        }
        
        count++;
        
        if (count < skipCount + pageSize) {
          cursor.continue();
        } else {
          resolve(users);
        }
      } else {
        resolve(users);
      }
    };
    
    request.onerror = () => reject(request.error);
  });
}
5.2.3 索引迭代与性能优化
// 使用索引高效查询
function findUserByEmail(db, email) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const index = transaction.objectStore('users').index('email');
    
    // 使用get()直接获取,比游标更高效
    const request = index.get(email);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

六、事务管理与并发控制

6.1 事务生命周期
const transaction = db.transaction(['users'], 'readwrite');

// 事务创建时的状态:active

// 执行数据操作
const objectStore = transaction.objectStore('users');
objectStore.add({ id: 1, name: 'September' });

// 事务完成时
transaction.oncomplete = () => console.log('事务已提交');

// 事务失败时
transaction.onerror = () => console.log('事务已回滚');

// 事务被中止时
transaction.onabort = () => console.log('事务已中止');

// 手动中止事务
// transaction.abort();
6.2 并发控制

IndexedDB 使用乐观并发控制,当两个事务同时修改同一数据时:

  1. 第一个提交的事务会成功。

  2. 第二个提交的事务会失败并触发 VersionError。

// 处理并发冲突
async function updateUserSafely(db, userId, updateFn) {
  while (true) {
    try {
      const transaction = db.transaction(['users'], 'readwrite');
      const objectStore = transaction.objectStore('users');
      
      // 读取当前数据
      const user = await new Promise((resolve, reject) => {
        const request = objectStore.get(userId);
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
      
      // 应用更新
      const updatedUser = updateFn(user);
      
      // 保存更新
      await new Promise((resolve, reject) => {
        const request = objectStore.put(updatedUser);
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });
      
      return updatedUser;
    } catch (error) {
      if (error.name === 'VersionError') {
        // 并发冲突,重试
        continue;
      }
      throw error;
    }
  }
}

七、高级数据库设计

7.1 数据库版本管理
function openDatabase(name, version, upgradeCallback) {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(name, version);
    
    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      const oldVersion = event.oldVersion;
      
      if (upgradeCallback) {
        upgradeCallback(db, oldVersion, version);
      }
    };
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 使用示例
openDatabase('myDB', 2, (db, oldVersion, newVersion) => {
  if (oldVersion < 1) {
    // 版本1:创建users对象存储
    const usersStore = db.createObjectStore('users', { keyPath: 'id' });
    usersStore.createIndex('email', 'email', { unique: true });
  }
  
  if (oldVersion < 2) {
    // 版本2:创建posts对象存储
    const postsStore = db.createObjectStore('posts', { keyPath: 'id' });
    postsStore.createIndex('userId', 'userId', { unique: false });
    postsStore.createIndex('createdAt', 'createdAt', { unique: false });
  }
});
7.2 关系数据建模
// 一对多关系示例:用户-文章
function setupRelationalDatabase(db) {
  // 创建用户存储
  const usersStore = db.createObjectStore('users', { keyPath: 'id' });
  
  // 创建文章存储
  const postsStore = db.createObjectStore('posts', { keyPath: 'id' });
  
  // 创建文章的用户ID索引,用于查询用户的所有文章
  postsStore.createIndex('userId', 'userId', { unique: false });
  
  // 创建文章的发布时间索引
  postsStore.createIndex('publishedAt', 'publishedAt', { unique: false });
}

// 查询用户的所有文章
function getUserPosts(db, userId) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['posts'], 'readonly');
    const index = transaction.objectStore('posts').index('userId');
    const posts = [];
    
    const request = index.openCursor(IDBKeyRange.only(userId));
    
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        posts.push(cursor.value);
        cursor.continue();
      } else {
        resolve(posts);
      }
    };
    
    request.onerror = () => reject(request.error);
  });
}

八、性能优化与最佳实践

8.1 批量操作优化
// 批量添加数据
function bulkAddUsers(db, users) {
  return new Promise((resolve, reject) => {
    // 使用单个事务处理所有添加操作
    const transaction = db.transaction(['users'], 'readwrite');
    const objectStore = transaction.objectStore('users');
    
    // 为每个用户创建添加请求
    users.forEach(user => {
      objectStore.add(user);
    });
    
    transaction.oncomplete = () => resolve();
    transaction.onerror = () => reject(transaction.error);
  });
}
8.2 索引设计原则
  • 只为频繁查询的属性创建索引。

  • 唯一索引比非唯一索引查询更快。

  • 避免创建过多索引(会影响写入性能)。

  • 复合索引应将最常用的查询条件放在前面

8.3 内存管理
// 处理大量数据时避免内存溢出
function processLargeDataset(db) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['largeData'], 'readonly');
    const objectStore = transaction.objectStore('largeData');
    let processedCount = 0;
    
    const request = objectStore.openCursor();
    
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      
      if (cursor) {
        // 处理当前记录
        processRecord(cursor.value);
        processedCount++;
        
        // 释放当前记录的引用
        cursor.value = null;
        
        // 继续处理下一条记录
        cursor.continue();
      } else {
        resolve(processedCount);
      }
    };
    
    request.onerror = () => reject(request.error);
  });
}

九、实际应用案例

9.1 离线数据同步
class OfflineSyncManager {
  constructor(dbName) {
    this.dbName = dbName;
    this.db = null;
  }
  
  async init() {
    this.db = await this.openDatabase();
  }
  
  async openDatabase() {
    // 打开数据库并创建必要的存储
    // ...
  }
  
  async saveOfflineData(operation, data) {
    // 保存离线操作到数据库
    const transaction = this.db.transaction(['offlineOperations'], 'readwrite');
    const store = transaction.objectStore('offlineOperations');
    
    const operationRecord = {
      id: Date.now() + Math.random(),
      operation,
      data,
      timestamp: Date.now(),
      status: 'pending'
    };
    
    await new Promise((resolve, reject) => {
      const request = store.add(operationRecord);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
  
  async syncData() {
    // 同步所有待处理的离线操作
    const pendingOperations = await this.getPendingOperations();
    
    for (const operation of pendingOperations) {
      try {
        // 尝试同步到服务器
        await this.syncToServer(operation);
        
        // 同步成功,更新状态
        await this.updateOperationStatus(operation.id, 'completed');
      } catch (error) {
        // 同步失败,更新状态并重试
        await this.updateOperationStatus(operation.id, 'failed');
      }
    }
  }
  
  // 其他方法...
}
9.2 全文搜索实现
// 创建全文搜索索引
function createFullTextIndex(objectStore, fieldNames) {
  // 为每个字段创建索引
  fieldNames.forEach(fieldName => {
    objectStore.createIndex(`fulltext_${fieldName}`, fieldName, { unique: false });
  });
}

// 简单的全文搜索实现
async function fullTextSearch(db, storeName, query) {
  const transaction = db.transaction([storeName], 'readonly');
  const store = transaction.objectStore(storeName);
  const results = new Set();
  
  // 搜索所有全文索引字段
  const indexNames = Array.from(store.indexNames).filter(name => 
    name.startsWith('fulltext_')
  );
  
  for (const indexName of indexNames) {
    const index = store.index(indexName);
    const fieldName = indexName.replace('fulltext_', '');
    
    // 使用游标范围查询匹配的记录
    const range = IDBKeyRange.bound(query, query + '\uffff');
    const cursorRequest = index.openCursor(range);
    
    await new Promise((resolve) => {
      cursorRequest.onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
          results.add(cursor.primaryKey);
          cursor.continue();
        } else {
          resolve();
        }
      };
    });
  }
  
  // 获取完整的记录数据
  const finalResults = [];
  for (const key of results) {
    const record = await new Promise((resolve, reject) => {
      const request = store.get(key);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
    finalResults.push(record);
  }
  
  return finalResults;
}

十、常见问题与解决方案

10.1 浏览器兼容性
// 检测 IndexedDB 支持
function checkIndexedDBSupport() {
  if (!('indexedDB' in window)) {
    console.error('此浏览器不支持 IndexedDB');
    return false;
  }
  
  // 检测特定功能支持
  try {
    // 检测复合索引支持
    const db = indexedDB.open('testCompositeIndex', 1);
    db.onupgradeneeded = (event) => {
      const db = event.target.result;
      const store = db.createObjectStore('test', { keyPath: 'id' });
      store.createIndex('composite', ['a', 'b'], { unique: false });
      db.close();
      indexedDB.deleteDatabase('testCompositeIndex');
      console.log('支持复合索引');
    };
  } catch (error) {
    console.log('不支持复合索引');
  }
  
  return true;
}
10.2 安全问题
  • 不要存储敏感信息:IndexedDB 数据可被用户访问和修改。

  • 使用加密:对敏感数据进行加密后再存储。

  • 验证数据:从 IndexedDB 读取数据后进行验证。

// 简单的数据加密存储
function encryptData(data, key) {
  // 使用 Web Crypto API 进行加密
  // ...
}

function decryptData(encryptedData, key) {
  // 使用 Web Crypto API 进行解密
  // ...
}

async function secureStore(db, storeName, data, encryptionKey) {
  const encryptedData = await encryptData(data, encryptionKey);
  return await this.storeData(db, storeName, encryptedData);
}

async function secureRetrieve(db, storeName, key, encryptionKey) {
  const encryptedData = await this.retrieveData(db, storeName, key);
  return await decryptData(encryptedData, encryptionKey);
}

十一、IndexedDB 与其他存储方案的比较

11.1 与 Web Storage 的比较

特性

IndexedDB

localStorage

sessionStorage

存储容量

几百 MB 到 GB

约 5MB

约 5MB

数据类型

复杂结构化数据

仅字符串

仅字符串

操作方式

异步

同步

同步

事务支持

支持

不支持

不支持

索引查询

支持

不支持

不支持

适用场景

大量结构化数据

简单键值对

会话数据

11.2 与 Cache API 的比较

特性

IndexedDB

Cache API

用途

结构化数据存储

HTTP 响应缓存

API 复杂度

查询能力

强大

有限(基于请求 URL)

数据类型

任意 JavaScript 对象

Response 对象

适用场

应用数据存储

离线资源缓存

总结

IndexedDB 是浏览器中功能最强大的客户端存储 API,它提供了异步操作、事务支持、索引查询等高级特性,适用于需要存储大量结构化数据的 Web 应用。通过合理的数据库设计、索引优化和事务管理,可以充分发挥 IndexedDB 的性能优势,为用户提供流畅的离线体验和高性能的数据处理能力。