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 的性能优势,为用户提供流畅的离线体验和高性能的数据处理能力。