人人都会AI编程

17.3 IndexedDB:浏览器端数据库

更新时间:2026-07-11

当需要存储的数据超出 Web Storage 所能承载的量级,或需要更复杂的查询支持时,IndexedDB 便是浏览器端的“重型武器”。它本质上是一个运行在浏览器内的、事务化的、基于键值对的对象数据库。

为什么需要 IndexedDB

在前面两节中,我们讨论了 Cookie 和 Web Storage。它们都有共同的上限:容量小(通常 5–10 MB)、只能存储字符串、没有索引、不支持复杂查询。IndexedDB 则完全不同:

  • 大容量:单个源(origin)的存储限额通常可达数百 MB 甚至 GB 级别,具体取决于浏览器和磁盘空间。这为离线缓存大量数据提供了基础。
  • 结构化存储:数据库存储的不是字符串,而是 JavaScript 对象(包括数组、嵌套对象、二进制数据等),经过结构化克隆算法保存和读取。
  • 异步设计:所有读写操作都是异步的,不会阻塞主线程,因此对大数据的处理不会导致页面卡顿。
  • 支持索引与查询:可以创建索引,基于索引进行范围查询、排序,支持游标(cursor)遍历,这在 Web Storage 中完全无法实现。
  • 事务机制:所有数据的读写必须在一个事务内完成,保证操作的一致性和原子性。事务有只读、读写版本之分。

核心概念速览

理解 IndexedDB 不需要像理解传统关系型数据库那样漫长,但仍有几个关键概念需要把握。

| 概念 | 简介 |
|------|------|
| 数据库(Database) | 一个域名下可以创建多个数据库,每个数据库有自己独立的名称和版本。 |
| 对象仓库(Object Store) | 类似关系型数据库中的“表”,用来存放一类数据对象。每个仓库有一个主键(key path 或 key generator)。 |
| 索引(Index) | 建立在对象属性上的查询入口,用于快速检索非主键字段。 |
| 事务(Transaction) | 所有操作必须放在一个事务内,事务类型(readonly / readwrite)决定了并发能力。 |
| 请求(Request) | 异步操作的返回载体,通过 onsuccess / onerror 事件获取结果或捕获错误。 |

基本操作:从打开到增删改查

IndexedDB 的原生 API 采用回调(事件)风格,代码量相对较多。下面展示一个典型的读写流程,并同时给出基于 Promise 的现代封装(原生 API 现在已经支持部分 Promise,但并非所有浏览器完全覆盖,因此常用类库或自定义封装)。

打开数据库并创建对象仓库

// 打开(或创建)数据库,版本号决定是否触发升级事件
const request = indexedDB.open('MyAppDB', 1);

// 数据库版本更新时触发(首次创建或升级版本号大于现有版本)
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // 创建一个对象仓库,主键为 id 属性,自动递增
  const store = db.createObjectStore('users', {
    keyPath: 'id',
    autoIncrement: true
  });
  // 为 name 字段创建索引,允许非唯一值
  store.createIndex('nameIndex', 'name', { unique: false });
};

request.onsuccess = () => {
  console.log('数据库打开成功');
  const db = request.result;
  // 后续操作在此进行
};

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

添加数据(需在事务中)

function addUser(db, user) {
  // 创建读写事务,指定操作的对象仓库
  const transaction = db.transaction('users', 'readwrite');
  const store = transaction.objectStore('users');
  const addRequest = store.add(user);

  addRequest.onsuccess = () => {
    console.log('用户添加成功,id:', addRequest.result);
  };

  addRequest.onerror = () => {
    console.error('添加失败', addRequest.error);
  };
}

读取数据

function getUser(db, id) {
  const transaction = db.transaction('users', 'readonly');
  const store = transaction.objectStore('users');
  const getRequest = store.get(id);

  getRequest.onsuccess = () => {
    console.log('查询结果:', getRequest.result);
  };
}

使用索引查询

function getUserByName(db, name) {
  const transaction = db.transaction('users', 'readonly');
  const store = transaction.objectStore('users');
  const index = store.index('nameIndex');
  const query = index.get(name); // 返回第一个匹配项

  query.onsuccess = () => {
    console.log('通过 name 索引找到:', query.result);
  };

  // 若要获取多个结果,可使用游标
  const keyRange = IDBKeyRange.only(name);
  index.openCursor(keyRange).onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      console.log(cursor.value);
      cursor.continue();
    }
  };
}

更新与删除

更新使用 put(会覆盖同主键记录),删除使用 delete,方法同在事务内的对象仓库上调用,完全类似 addget

异步痛点与 Promise 化封装

原生 IndexedDB 基于事件回调,容易形成“回调地狱”。在实际项目中,我们通常使用成熟的封装库,如 idb(Google 推出的轻量级 Promise 封装)或 localForage(将 IndexedDB 降级为 Web Storage 的统一接口)。也可以用简单的辅助函数手动 Promise 化:

function promisify(request) {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 使用
const db = await promisify(indexedDB.open('test', 1));
const tx = db.transaction('store', 'readwrite');
const store = tx.objectStore('store');
await promisify(store.add({ name: 'Alice' }));

真实场景应用

IndexedDB 不适合用来替代后端数据库,但在前端领域,它在以下场景中价值很高:

  • 离线应用(PWA):缓存大量页面资源、用户生成内容、草稿等。结合 Service Worker,可以实现完全离线的 Web 应用。
  • 大量结构化数据的本地处理:例如管理数千条联系人、邮件、待办事项的数据,并提供排序、搜索功能。
  • 数据分析工具:将接口返回的大量 JSON 数据存储到 IndexedDB,利用索引和游标进行前端侧的过滤分析,避免频繁请求服务器。
  • 游戏素材缓存:Web 游戏中角色、地图等资源大可不必每次加载,直接存储到 IndexedDB 中,减少重复下载。

使用注意事项

  • 容量并非无限:各浏览器策略不同,超出额度后会触发配额管理弹窗或拒绝写入,应做好错误处理。
  • 事务生命周期:事务在浏览器事件循环的一次微任务完成后自动提交,所有操作必须在事务创建后尽快执行,不能跨异步间隔,否则会触发 TransactionInactiveError
  • 版本管理:添加或删除对象仓库、修改索引必须通过提升数据库版本号来实现,这需要谨慎操作,尤其是在线上多用户同时打开页面时。
  • 隐私模式下行为有差异:部分浏览器在隐私模式下不支持 IndexedDB 或大幅限制存储容量。

IndexedDB 的 API 虽然原始,但它为现代 Web 应用提供了接近原生应用的离线能力和数据管理能力。掌握它的基础操作,并善用社区封装,就能在需要存储大量结构化前端数据时游刃有余。